import { useEffect, useMemo, useState, useRef, useCallback } from 'react'; import { faMagnifyingGlass, faPlay, faPause, faSparkles } from '@fortawesome/pro-duotone-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faIcon } from '@/lib/icon-wrapper'; import type { SortDescriptor } from 'react-aria-components'; const SearchLg = faIcon(faMagnifyingGlass); import { Badge } from '@/components/base/badges/badges'; import { Input } from '@/components/base/input/input'; import { Table } from '@/components/application/table/table'; import { PaginationPageDefault } from '@/components/application/pagination/pagination'; import { PageHeader } from '@/components/layout/page-header'; import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle'; import { PhoneActionCell } from '@/components/call-desk/phone-action-cell'; import { RecordingAnalysisSlideout } from '@/components/call-desk/recording-analysis'; import { apiClient } from '@/lib/api-client'; import { getSocket } from '@/lib/socket'; import { formatPhone, formatDateOrdinal, formatTimeOnly } from '@/lib/format'; import { computeSlaStatus } from '@/lib/scoring'; import { cx } from '@/utils/cx'; type RecordingRecord = { id: string; createdAt: string | null; direction: string | null; callStatus: string | null; callerNumber: { primaryPhoneNumber: string } | null; agentName: string | null; agent: { id: string; name: string; ozonetelAgentId: string } | null; startedAt: string | null; durationSec: number | null; disposition: string | null; recording: { primaryLinkUrl: string; primaryLinkLabel: string } | null; sla: number | null; }; const QUERY = `{ calls(first: 200, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { id createdAt direction callStatus callerNumber { primaryPhoneNumber } agentName agent { id name ozonetelAgentId } startedAt durationSec disposition sla recording { primaryLinkUrl primaryLinkLabel } } } } }`; const PAGE_SIZE = 15; const formatDuration = (sec: number | null): string => { if (!sec) return '—'; const m = Math.floor(sec / 60); const s = sec % 60; return `${m}:${s.toString().padStart(2, '0')}`; }; const getCallSla = (call: RecordingRecord): { 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 = ({ url }: { url: string }) => { const audioRef = useRef(null); const [playing, setPlaying] = useState(false); const toggle = () => { if (!audioRef.current) return; if (playing) { audioRef.current.pause(); } else { audioRef.current.play(); } setPlaying(!playing); }; return (
); }; const columnDefs = [ { id: 'agent', label: 'Agent', defaultVisible: true, allowsSorting: true, isRowHeader: true }, { id: 'caller', label: 'Caller', defaultVisible: true }, { id: 'ai', label: 'AI', defaultVisible: true }, { id: 'type', label: 'Type', defaultVisible: true }, { id: 'sla', label: 'SLA', defaultVisible: true, allowsSorting: true }, { id: 'dateTime', label: 'Date & Time', defaultVisible: true, allowsSorting: true }, { id: 'duration', label: 'Duration', defaultVisible: true, allowsSorting: true }, { id: 'disposition', label: 'Disposition', defaultVisible: true }, { id: 'recording', label: 'Recording', defaultVisible: true }, ]; export const CallRecordingsPage = () => { const [calls, setCalls] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [slideoutCallId, setSlideoutCallId] = useState(null); const [sortDescriptor, setSortDescriptor] = useState({ column: 'dateTime', direction: 'descending' }); const { visibleColumns, toggle: toggleColumn } = useColumnVisibility(columnDefs); // Dynamic columns for React Aria — filter by visibility, pass as prop const activeColumns = useMemo( () => columnDefs.filter(c => visibleColumns.has(c.id)), [visibleColumns], ); // Cell renderer — lives inside the component so it can access setSlideoutCallId const renderRecordingCell = useCallback((call: RecordingRecord, colId: string) => { const phone = call.callerNumber?.primaryPhoneNumber ?? ''; const dirLabel = call.direction === 'INBOUND' ? 'In' : 'Out'; const dirColor: 'blue' | 'brand' = call.direction === 'INBOUND' ? 'blue' : 'brand'; switch (colId) { case 'agent': return {call.agent?.name ?? call.agentName ?? '—'}; case 'caller': return phone ? : ; case 'ai': return ( { e.stopPropagation(); let longPressed = false; const timer = setTimeout(() => { longPressed = true; window.dispatchEvent(new CustomEvent('maint:trigger', { detail: 'clearAnalysisCache' })); }, 1000); const up = () => { clearTimeout(timer); if (!longPressed) setSlideoutCallId(call.id); }; document.addEventListener('pointerup', up, { once: true }); }} className="inline-flex items-center gap-1 rounded-full bg-brand-primary px-2.5 py-1 text-xs font-semibold text-brand-secondary hover:bg-brand-secondary hover:text-white cursor-pointer transition duration-100 ease-linear" title="AI Analysis (long-press to regenerate)" > AI ); case 'type': return {dirLabel}; case 'sla': { const sla = getCallSla(call); if (!sla) return ; return ( {sla.percent}% ); } case 'dateTime': return call.startedAt ? (
{formatDateOrdinal(call.startedAt)} {formatTimeOnly(call.startedAt)}
) : ; case 'duration': return {formatDuration(call.durationSec)}; case 'disposition': return call.disposition ? {call.disposition.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, c => c.toUpperCase())} : ; case 'recording': return call.recording?.primaryLinkUrl ? : null; default: return null; } }, []); const fetchRecordings = useCallback(() => { apiClient.graphql<{ calls: { edges: Array<{ node: RecordingRecord }> } }>(QUERY, undefined, { silent: true }) .then(data => { const withRecordings = data.calls.edges .map(e => e.node) .filter(c => c.recording?.primaryLinkUrl); setCalls(withRecordings); }) .catch(() => {}) .finally(() => setLoading(false)); }, []); useEffect(() => { fetchRecordings(); // Listen for real-time call created events via WebSocket const socket = getSocket(); if (!socket.connected) socket.connect(); socket.emit('supervisor:register'); const handleCallCreated = () => fetchRecordings(); socket.on('call:created', handleCallCreated); return () => { socket.off('call:created', handleCallCreated); }; }, [fetchRecordings]); const filtered = useMemo(() => { let result = calls; if (search.trim()) { const q = search.toLowerCase(); result = result.filter(c => (c.agent?.name ?? c.agentName ?? '').toLowerCase().includes(q) || (c.callerNumber?.primaryPhoneNumber ?? '').includes(q) || (c.disposition ?? '').toLowerCase().includes(q), ); } // Sort if (sortDescriptor.column) { const dir = sortDescriptor.direction === 'ascending' ? 1 : -1; result = [...result].sort((a, b) => { switch (sortDescriptor.column) { case 'agent': return (a.agent?.name ?? a.agentName ?? '').localeCompare(b.agent?.name ?? b.agentName ?? '') * dir; case 'dateTime': { const ta = a.startedAt ? new Date(a.startedAt).getTime() : 0; const tb = b.startedAt ? new Date(b.startedAt).getTime() : 0; return (ta - tb) * dir; } case 'duration': return ((a.durationSec ?? 0) - (b.durationSec ?? 0)) * dir; case 'sla': return ((a.sla ?? 0) - (b.sla ?? 0)) * dir; default: return 0; } }); } return result; }, [calls, search, sortDescriptor]); const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); const pagedRows = filtered.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE); const handleSearch = (val: string) => { setSearch(val); setCurrentPage(1); }; return (
} /> {/* Table */}
{loading ? (

Loading recordings...

) : filtered.length === 0 ? (

{search ? 'No matching recordings' : 'No call recordings found'}

) : (
{(col) => ( )} {(call) => ( {(col) => ( {renderRecordingCell(call, col.id)} )} )}
)}
{/* Pagination */} {totalPages > 1 && (
)} {/* Analysis slideout */} {(() => { const call = slideoutCallId ? filtered.find(c => c.id === slideoutCallId) : null; if (!call?.recording?.primaryLinkUrl) return null; return ( { if (!open) setSlideoutCallId(null); }} recordingUrl={call.recording.primaryLinkUrl} callId={call.id} agentName={call.agentName} callerNumber={call.callerNumber?.primaryPhoneNumber ?? null} direction={call.direction} startedAt={call.startedAt} durationSec={call.durationSec} disposition={call.disposition} /> ); })()}
); };