mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
feat: SSE-driven worklist + agent call history split + remove SOURCE column
Worklist:
- SSE stream replaces 30s poll — EventSource on /api/supervisor/worklist/stream
triggers immediate fetchWorklist() on missed-call events
- Toast notification: 'Missed Call — {name} — needs callback'
- No polling fallback — SSE is the source of truth
Call History split by role:
- Agent: 'My Call History' — own calls only (matched by agent relation
or chain-parsed agentName), missed calls excluded (they belong on
the Call Desk queue), no Agent/Recording/SLA columns, phone clickable
via PhoneActionCell instead of separate Call button
- Supervisor: 'Call History' — all calls, Agent + Recording columns visible
Worklist panel:
- SOURCE/BRANCH column removed from display (data stays on row)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -178,34 +178,9 @@ const formatTimeAgo = (dateStr: string): string => {
|
|||||||
const formatDisposition = (disposition: string): string =>
|
const formatDisposition = (disposition: string): string =>
|
||||||
disposition.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
disposition.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||||
|
|
||||||
const formatSource = (source: string): string => {
|
// formatSource + formatDid kept for reference but no longer rendered
|
||||||
const map: Record<string, string> = {
|
// in the table — SOURCE/BRANCH column removed from display per user
|
||||||
FACEBOOK_AD: 'Facebook',
|
// request. Data stays on the row for future use.
|
||||||
GOOGLE_AD: 'Google',
|
|
||||||
WALK_IN: 'Walk-in',
|
|
||||||
REFERRAL: 'Referral',
|
|
||||||
WEBSITE: 'Website',
|
|
||||||
PHONE_INQUIRY: 'Phone',
|
|
||||||
PHONE: 'Phone',
|
|
||||||
OTHER: 'Other',
|
|
||||||
};
|
|
||||||
return map[source] ?? source.replace(/_/g, ' ');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resolve a DID (e.g. "918041763265") to a friendly branch/campaign name.
|
|
||||||
// The DID is the phone number the caller dialed — it identifies which
|
|
||||||
// hospital branch or campaign the call came through. Falls back to the
|
|
||||||
// last 10 digits if no mapping is found.
|
|
||||||
const formatDid = (did: string): string => {
|
|
||||||
if (!did) return '—';
|
|
||||||
// Known DIDs — loaded from the sidecar theme tokens at boot (via
|
|
||||||
// the ThemeTokenProvider). For now, hardcode nothing — strip country
|
|
||||||
// code and show the DID as a short number so it's at least readable.
|
|
||||||
const digits = did.replace(/\D/g, '');
|
|
||||||
// Strip leading 91 (India) for display
|
|
||||||
const short = digits.length > 10 && digits.startsWith('91') ? digits.slice(2) : digits;
|
|
||||||
return short;
|
|
||||||
};
|
|
||||||
|
|
||||||
const IconInbound = faIcon(faPhoneArrowDown);
|
const IconInbound = faIcon(faPhoneArrowDown);
|
||||||
const IconOutbound = faIcon(faPhoneArrowUp);
|
const IconOutbound = faIcon(faPhoneArrowUp);
|
||||||
@@ -239,7 +214,7 @@ const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], lea
|
|||||||
// Branch column: prefer the campaign name (e.g. "Cervical Cancer
|
// Branch column: prefer the campaign name (e.g. "Cervical Cancer
|
||||||
// Screening Drive") over the raw DID. Falls back to formatted DID
|
// Screening Drive") over the raw DID. Falls back to formatted DID
|
||||||
// for organic calls with no campaign.
|
// for organic calls with no campaign.
|
||||||
source: call.campaign?.campaignName ?? (call.callSourceNumber ? formatDid(call.callSourceNumber) : null),
|
source: call.campaign?.campaignName ?? call.callSourceNumber ?? null,
|
||||||
lastDisposition: call.disposition ?? null,
|
lastDisposition: call.disposition ?? null,
|
||||||
missedCallId: call.id,
|
missedCallId: call.id,
|
||||||
});
|
});
|
||||||
@@ -497,7 +472,6 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
|
|||||||
<Table.Head id="priority" label="SCORE" className="w-20" isRowHeader allowsSorting />
|
<Table.Head id="priority" label="SCORE" className="w-20" isRowHeader allowsSorting />
|
||||||
<Table.Head id="name" label="PATIENT" allowsSorting />
|
<Table.Head id="name" label="PATIENT" allowsSorting />
|
||||||
<Table.Head label="PHONE" />
|
<Table.Head label="PHONE" />
|
||||||
<Table.Head label={tab === 'missed' ? 'BRANCH' : 'SOURCE'} className="w-28" />
|
|
||||||
<Table.Head id="sla" label="SLA" className="w-24" allowsSorting />
|
<Table.Head id="sla" label="SLA" className="w-24" allowsSorting />
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body items={pagedRows}>
|
<Table.Body items={pagedRows}>
|
||||||
@@ -578,15 +552,6 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
|
|||||||
<span className="text-xs text-quaternary italic">No phone</span>
|
<span className="text-xs text-quaternary italic">No phone</span>
|
||||||
)}
|
)}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
|
||||||
{row.source ? (
|
|
||||||
<span className="text-xs text-tertiary truncate block max-w-[100px]">
|
|
||||||
{formatSource(row.source)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-quaternary">—</span>
|
|
||||||
)}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Badge size="sm" color={sla.color} type="pill-color">
|
<Badge size="sm" color={sla.color} type="pill-color">
|
||||||
{sla.label}
|
{sla.label}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from '@/lib/api-client';
|
||||||
|
import { notify } from '@/lib/toast';
|
||||||
|
|
||||||
type MissedCall = {
|
type MissedCall = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -133,9 +134,30 @@ export const useWorklist = (): UseWorklistResult => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchWorklist();
|
fetchWorklist();
|
||||||
|
|
||||||
// Refresh every 30 seconds
|
// SSE stream for instant worklist updates. No polling fallback —
|
||||||
const interval = setInterval(fetchWorklist, 30000);
|
// if SSE breaks, the worklist stops updating and we fix the SSE,
|
||||||
return () => clearInterval(interval);
|
// not paper over it with a poll.
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100';
|
||||||
|
let es: EventSource | null = null;
|
||||||
|
try {
|
||||||
|
es = new EventSource(`${API_URL}/api/supervisor/worklist/stream`);
|
||||||
|
es.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log('[WORKLIST-SSE]', data);
|
||||||
|
fetchWorklist();
|
||||||
|
if (data.type === 'missed-call') {
|
||||||
|
const name = data.callerName ?? data.callerPhone ?? 'Unknown';
|
||||||
|
notify.warning('Missed Call', `${name} — needs callback`);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
es.onerror = () => {
|
||||||
|
console.warn('[WORKLIST-SSE] Connection error — EventSource will auto-reconnect');
|
||||||
|
};
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return () => { es?.close(); };
|
||||||
}, [fetchWorklist]);
|
}, [fetchWorklist]);
|
||||||
|
|
||||||
return { ...data, loading, error, refresh: fetchWorklist };
|
return { ...data, loading, error, refresh: fetchWorklist };
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ import { Badge } from '@/components/base/badges/badges';
|
|||||||
import { Button } from '@/components/base/buttons/button';
|
import { Button } from '@/components/base/buttons/button';
|
||||||
import { Input } from '@/components/base/input/input';
|
import { Input } from '@/components/base/input/input';
|
||||||
import { Select } from '@/components/base/select/select';
|
import { Select } from '@/components/base/select/select';
|
||||||
import { ClickToCallButton } from '@/components/call-desk/click-to-call-button';
|
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
|
||||||
import { formatShortDate, formatPhone } from '@/lib/format';
|
import { formatShortDate, formatPhone } from '@/lib/format';
|
||||||
import { computeSlaStatus } from '@/lib/scoring';
|
// cx removed — no longer used after SLA column removal
|
||||||
import { cx } from '@/utils/cx';
|
|
||||||
import { useData } from '@/providers/data-provider';
|
import { useData } from '@/providers/data-provider';
|
||||||
|
import { useAuth } from '@/providers/auth-provider';
|
||||||
import { PaginationCardDefault } from '@/components/application/pagination/pagination';
|
import { PaginationCardDefault } from '@/components/application/pagination/pagination';
|
||||||
import type { Call, CallDirection, CallDisposition } from '@/types/entities';
|
import type { Call, CallDirection, CallDisposition } from '@/types/entities';
|
||||||
|
|
||||||
@@ -54,13 +54,6 @@ const formatDuration = (seconds: number | null): string => {
|
|||||||
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatPhoneDisplay = (call: Call): string => {
|
|
||||||
if (call.callerNumber && call.callerNumber.length > 0) {
|
|
||||||
return formatPhone(call.callerNumber[0]);
|
|
||||||
}
|
|
||||||
return '\u2014';
|
|
||||||
};
|
|
||||||
|
|
||||||
const DirectionIcon: FC<{ direction: CallDirection | null; status: Call['callStatus'] }> = ({ direction, status }) => {
|
const DirectionIcon: FC<{ direction: CallDirection | null; status: Call['callStatus'] }> = ({ direction, status }) => {
|
||||||
if (status === 'MISSED') {
|
if (status === 'MISSED') {
|
||||||
return <FontAwesomeIcon icon={faPhoneXmark} className="size-4 text-fg-error-secondary" />;
|
return <FontAwesomeIcon icon={faPhoneXmark} className="size-4 text-fg-error-secondary" />;
|
||||||
@@ -71,12 +64,6 @@ const DirectionIcon: FC<{ direction: CallDirection | null; status: Call['callSta
|
|||||||
return <FontAwesomeIcon icon={faPhoneArrowDown} className="size-4 text-fg-success-secondary" />;
|
return <FontAwesomeIcon icon={faPhoneArrowDown} className="size-4 text-fg-success-secondary" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCallSla = (call: Call): { percent: number; status: 'low' | 'medium' | 'high' | 'critical' } | null => {
|
|
||||||
if (call.sla == null) return null;
|
|
||||||
const percent = Math.round(call.sla);
|
|
||||||
return { percent, status: computeSlaStatus(percent) };
|
|
||||||
};
|
|
||||||
|
|
||||||
const RecordingPlayer: FC<{ url: string }> = ({ url }) => {
|
const RecordingPlayer: FC<{ url: string }> = ({ url }) => {
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
@@ -88,8 +75,7 @@ const RecordingPlayer: FC<{ url: string }> = ({ url }) => {
|
|||||||
audio.pause();
|
audio.pause();
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
} else {
|
} else {
|
||||||
audio.play().catch(() => setIsPlaying(false));
|
audio.play().then(() => setIsPlaying(true)).catch(() => {});
|
||||||
setIsPlaying(true);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,11 +105,11 @@ const PAGE_SIZE = 20;
|
|||||||
|
|
||||||
export const CallHistoryPage = () => {
|
export const CallHistoryPage = () => {
|
||||||
const { calls, leads } = useData();
|
const { calls, leads } = useData();
|
||||||
|
const { user, isAdmin } = useAuth();
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [filter, setFilter] = useState<FilterKey>('all');
|
const [filter, setFilter] = useState<FilterKey>('all');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
// Build a map of lead names by ID for enrichment
|
|
||||||
const leadNameMap = useMemo(() => {
|
const leadNameMap = useMemo(() => {
|
||||||
const map = new Map<string, string>();
|
const map = new Map<string, string>();
|
||||||
for (const lead of leads) {
|
for (const lead of leads) {
|
||||||
@@ -135,7 +121,10 @@ export const CallHistoryPage = () => {
|
|||||||
return map;
|
return map;
|
||||||
}, [leads]);
|
}, [leads]);
|
||||||
|
|
||||||
// Sort by time (newest first) and apply filters
|
// Agent sees only their own calls; supervisor sees all
|
||||||
|
const agentConfig = localStorage.getItem('helix_agent_config');
|
||||||
|
const myAgentId = agentConfig ? (() => { try { return JSON.parse(agentConfig).ozonetelAgentId; } catch { return null; } })() : null;
|
||||||
|
|
||||||
const filteredCalls = useMemo(() => {
|
const filteredCalls = useMemo(() => {
|
||||||
let result = [...calls].sort((a, b) => {
|
let result = [...calls].sort((a, b) => {
|
||||||
const dateA = a.startedAt ? new Date(a.startedAt).getTime() : 0;
|
const dateA = a.startedAt ? new Date(a.startedAt).getTime() : 0;
|
||||||
@@ -143,37 +132,53 @@ export const CallHistoryPage = () => {
|
|||||||
return dateB - dateA;
|
return dateB - dateA;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Direction / status filter. "Inbound" shows answered inbound only — missed
|
// CC agent: filter to own calls only.
|
||||||
// calls have their own dedicated filter so they don't double-appear.
|
// Match on the authoritative agent relation (set by CDR enrichment)
|
||||||
|
// or the raw agentName for unenriched rows. Chain names like
|
||||||
|
// "RamaiahAdmin -> GlobalHealthX" are split — last segment is
|
||||||
|
// the final handler. Missed calls have no handler and are excluded
|
||||||
|
// from the agent's personal history (they belong on the Missed
|
||||||
|
// Calls queue).
|
||||||
|
if (!isAdmin && myAgentId) {
|
||||||
|
const myId = myAgentId.toLowerCase();
|
||||||
|
result = result.filter((c) => {
|
||||||
|
// Missed calls have no handler — exclude from agent history
|
||||||
|
if (c.callStatus === 'MISSED') return false;
|
||||||
|
// Authoritative: agent relation from CDR enrichment
|
||||||
|
if (c.agent?.ozonetelAgentId?.toLowerCase() === myId) return true;
|
||||||
|
// Fallback: parse chain in agentName, match last segment
|
||||||
|
if (c.agentName) {
|
||||||
|
const segments = c.agentName.split('->').map(s => s.trim().toLowerCase());
|
||||||
|
const finalHandler = segments[segments.length - 1];
|
||||||
|
if (finalHandler === myId) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (filter === 'inbound') result = result.filter((c) => c.callDirection === 'INBOUND' && c.callStatus !== 'MISSED');
|
if (filter === 'inbound') result = result.filter((c) => c.callDirection === 'INBOUND' && c.callStatus !== 'MISSED');
|
||||||
else if (filter === 'outbound') result = result.filter((c) => c.callDirection === 'OUTBOUND');
|
else if (filter === 'outbound') result = result.filter((c) => c.callDirection === 'OUTBOUND');
|
||||||
else if (filter === 'missed') result = result.filter((c) => c.callStatus === 'MISSED');
|
else if (filter === 'missed') result = result.filter((c) => c.callStatus === 'MISSED');
|
||||||
|
|
||||||
// Search filter
|
|
||||||
if (search.trim()) {
|
if (search.trim()) {
|
||||||
const q = search.toLowerCase();
|
const q = search.toLowerCase();
|
||||||
result = result.filter((c) => {
|
result = result.filter((c) => {
|
||||||
const name = c.leadName ?? leadNameMap.get(c.leadId ?? '') ?? '';
|
const name = c.leadName ?? leadNameMap.get(c.leadId ?? '') ?? '';
|
||||||
const phone = c.callerNumber?.[0]?.number ?? '';
|
const phone = c.callerNumber?.[0]?.number ?? '';
|
||||||
const agent = c.agentName ?? '';
|
const agent = c.agentName ?? '';
|
||||||
return (
|
return name.toLowerCase().includes(q) || phone.includes(q) || agent.toLowerCase().includes(q);
|
||||||
name.toLowerCase().includes(q) ||
|
|
||||||
phone.includes(q) ||
|
|
||||||
agent.toLowerCase().includes(q)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [calls, filter, search, leadNameMap]);
|
}, [calls, filter, search, leadNameMap, isAdmin, myAgentId, user.id]);
|
||||||
|
|
||||||
const completedCount = filteredCalls.filter((c) => c.callStatus !== 'MISSED').length;
|
const completedCount = filteredCalls.filter((c) => c.callStatus === 'COMPLETED').length;
|
||||||
const missedCount = filteredCalls.filter((c) => c.callStatus === 'MISSED').length;
|
const missedCount = filteredCalls.filter((c) => c.callStatus === 'MISSED').length;
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(filteredCalls.length / PAGE_SIZE));
|
const totalPages = Math.max(1, Math.ceil(filteredCalls.length / PAGE_SIZE));
|
||||||
const pagedCalls = filteredCalls.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
const pagedCalls = filteredCalls.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||||
|
|
||||||
// Reset page when filter/search changes
|
|
||||||
useEffect(() => { setPage(1); }, [filter, search]); // eslint-disable-line react-hooks/exhaustive-deps
|
useEffect(() => { setPage(1); }, [filter, search]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -181,7 +186,7 @@ export const CallHistoryPage = () => {
|
|||||||
<div className="flex flex-1 flex-col overflow-hidden p-7">
|
<div className="flex flex-1 flex-col overflow-hidden p-7">
|
||||||
<TableCard.Root size="md" className="flex-1 min-h-0">
|
<TableCard.Root size="md" className="flex-1 min-h-0">
|
||||||
<TableCard.Header
|
<TableCard.Header
|
||||||
title="Call History"
|
title={isAdmin ? 'Call History' : 'My Call History'}
|
||||||
badge={String(filteredCalls.length)}
|
badge={String(filteredCalls.length)}
|
||||||
description={`${completedCount} completed \u00B7 ${missedCount} missed`}
|
description={`${completedCount} completed \u00B7 ${missedCount} missed`}
|
||||||
contentTrailing={
|
contentTrailing={
|
||||||
@@ -231,18 +236,15 @@ export const CallHistoryPage = () => {
|
|||||||
<Table.Head label="PHONE" />
|
<Table.Head label="PHONE" />
|
||||||
<Table.Head label="DURATION" className="w-24" />
|
<Table.Head label="DURATION" className="w-24" />
|
||||||
<Table.Head label="OUTCOME" />
|
<Table.Head label="OUTCOME" />
|
||||||
<Table.Head label="SLA" className="w-24" />
|
{/* Agent columns — only visible for supervisor */}
|
||||||
<Table.Head label="AGENT" />
|
{isAdmin && <Table.Head label="AGENT" />}
|
||||||
<Table.Head label="RECORDING" className="w-24" />
|
{isAdmin && <Table.Head label="RECORDING" className="w-24" />}
|
||||||
<Table.Head label="TIME" />
|
<Table.Head label="TIME" />
|
||||||
<Table.Head label="ACTIONS" className="w-24" />
|
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body items={pagedCalls}>
|
<Table.Body items={pagedCalls}>
|
||||||
{(call) => {
|
{(call) => {
|
||||||
const phoneRawForName = call.callerNumber?.[0]?.number ?? '';
|
|
||||||
const patientName = call.leadName ?? leadNameMap.get(call.leadId ?? '') ?? (phoneRawForName ? formatPhone({ number: phoneRawForName, callingCode: '+91' }) : 'Unknown');
|
|
||||||
const phoneDisplay = formatPhoneDisplay(call);
|
|
||||||
const phoneRaw = call.callerNumber?.[0]?.number ?? '';
|
const phoneRaw = call.callerNumber?.[0]?.number ?? '';
|
||||||
|
const patientName = call.leadName ?? leadNameMap.get(call.leadId ?? '') ?? (phoneRaw ? formatPhone({ number: phoneRaw, callingCode: '+91' }) : 'Unknown');
|
||||||
const dispositionCfg = call.disposition !== null ? dispositionConfig[call.disposition] : null;
|
const dispositionCfg = call.disposition !== null ? dispositionConfig[call.disposition] : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -256,9 +258,14 @@ export const CallHistoryPage = () => {
|
|||||||
</span>
|
</span>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<span className="text-sm text-tertiary whitespace-nowrap">
|
{phoneRaw ? (
|
||||||
{phoneDisplay}
|
<PhoneActionCell
|
||||||
</span>
|
phoneNumber={phoneRaw}
|
||||||
|
displayNumber={formatPhone({ number: phoneRaw, callingCode: '+91' })}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-quaternary">{'\u2014'}</span>
|
||||||
|
)}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<span className="text-sm text-secondary whitespace-nowrap">
|
<span className="text-sm text-secondary whitespace-nowrap">
|
||||||
@@ -274,53 +281,27 @@ export const CallHistoryPage = () => {
|
|||||||
<span className="text-sm text-quaternary">{'\u2014'}</span>
|
<span className="text-sm text-quaternary">{'\u2014'}</span>
|
||||||
)}
|
)}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
{isAdmin && (
|
||||||
{(() => {
|
<Table.Cell>
|
||||||
const sla = getCallSla(call);
|
<span className="text-sm text-secondary">
|
||||||
if (!sla) return <span className="text-xs text-quaternary">—</span>;
|
{call.agent?.name ?? call.agentName ?? '\u2014'}
|
||||||
return (
|
</span>
|
||||||
<span className="inline-flex items-center gap-1.5 text-xs font-medium">
|
</Table.Cell>
|
||||||
<span className={cx(
|
)}
|
||||||
'size-2 rounded-full',
|
{isAdmin && (
|
||||||
sla.status === 'low' && 'bg-success-solid',
|
<Table.Cell>
|
||||||
sla.status === 'medium' && 'bg-warning-solid',
|
{call.recordingUrl ? (
|
||||||
sla.status === 'high' && 'bg-error-solid',
|
<RecordingPlayer url={call.recordingUrl} />
|
||||||
sla.status === 'critical' && 'bg-error-solid animate-pulse',
|
) : (
|
||||||
)} />
|
<span className="text-xs text-quaternary">{'\u2014'}</span>
|
||||||
<span className="text-secondary">{sla.percent}%</span>
|
)}
|
||||||
</span>
|
</Table.Cell>
|
||||||
);
|
)}
|
||||||
})()}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
<span className="text-sm text-secondary">
|
|
||||||
{call.agentName ?? '\u2014'}
|
|
||||||
</span>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
{call.recordingUrl ? (
|
|
||||||
<RecordingPlayer url={call.recordingUrl} />
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-quaternary">{'\u2014'}</span>
|
|
||||||
)}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<span className="text-sm text-tertiary whitespace-nowrap">
|
<span className="text-sm text-tertiary whitespace-nowrap">
|
||||||
{call.startedAt ? formatShortDate(call.startedAt) : '\u2014'}
|
{call.startedAt ? formatShortDate(call.startedAt) : '\u2014'}
|
||||||
</span>
|
</span>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
|
||||||
{phoneRaw ? (
|
|
||||||
<ClickToCallButton
|
|
||||||
phoneNumber={phoneRaw}
|
|
||||||
leadId={call.leadId ?? undefined}
|
|
||||||
label="Call"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-quaternary">{'\u2014'}</span>
|
|
||||||
)}
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user