import { useEffect, useMemo, useState, useCallback } from 'react'; import { faMagnifyingGlass } from '@fortawesome/pro-duotone-svg-icons'; import type { SortDescriptor } from 'react-aria-components'; import { faIcon } from '@/lib/icon-wrapper'; 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 { Tabs, TabList, Tab } from '@/components/application/tabs/tabs'; import { PaginationPageDefault } from '@/components/application/pagination/pagination'; import { TopBar } from '@/components/layout/top-bar'; import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle'; import { PhoneActionCell } from '@/components/call-desk/phone-action-cell'; import { apiClient } from '@/lib/api-client'; import { formatPhone, formatDateOrdinal, formatTimeOnly } from '@/lib/format'; import { computeSlaStatus } from '@/lib/scoring'; import { cx } from '@/utils/cx'; type MissedCallRecord = { id: string; callerNumber: { primaryPhoneNumber: string } | null; agentName: string | null; startedAt: string | null; callSourceNumber: string | null; callbackStatus: string | null; missedCallCount: number | null; callbackAttemptedAt: string | null; sla: number | null; }; type StatusTab = 'all' | 'PENDING_CALLBACK' | 'CALLBACK_ATTEMPTED' | 'CALLBACK_COMPLETED'; const QUERY = `{ calls(first: 200, filter: { callStatus: { eq: MISSED } }, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { id callerNumber { primaryPhoneNumber } agentName startedAt callSourceNumber callbackStatus missedCallCount callbackAttemptedAt sla } } } }`; const PAGE_SIZE = 15; const STATUS_LABELS: Record = { PENDING_CALLBACK: 'Pending', CALLBACK_ATTEMPTED: 'Attempted', CALLBACK_COMPLETED: 'Completed', WRONG_NUMBER: 'Wrong Number', INVALID: 'Invalid', }; const STATUS_COLORS: Record = { PENDING_CALLBACK: 'warning', CALLBACK_ATTEMPTED: 'brand', CALLBACK_COMPLETED: 'success', WRONG_NUMBER: 'error', INVALID: 'gray', }; const columnDefs = [ { id: 'caller', label: 'Caller', defaultVisible: true }, { id: 'dateTime', label: 'Date & Time', defaultVisible: true }, { id: 'branch', label: 'Branch', defaultVisible: true }, { id: 'agent', label: 'Agent', defaultVisible: true }, { id: 'count', label: 'Count', defaultVisible: true }, { id: 'status', label: 'Status', defaultVisible: true }, { id: 'sla', label: 'SLA', defaultVisible: true }, { id: 'callback', label: 'Callback At', defaultVisible: false }, ]; export const MissedCallsPage = () => { const [calls, setCalls] = useState([]); const [loading, setLoading] = useState(true); const [tab, setTab] = useState('all'); const [search, setSearch] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [sortDescriptor, setSortDescriptor] = useState({ column: 'dateTime', direction: 'descending' }); const { visibleColumns, toggle: toggleColumn } = useColumnVisibility(columnDefs); const fetchCalls = useCallback(() => { apiClient.graphql<{ calls: { edges: Array<{ node: MissedCallRecord }> } }>(QUERY, undefined, { silent: true }) .then(data => setCalls(data.calls.edges.map(e => e.node))) .catch(() => {}) .finally(() => setLoading(false)); }, []); useEffect(() => { fetchCalls(); const interval = setInterval(fetchCalls, 30000); return () => clearInterval(interval); }, [fetchCalls]); const statusCounts = useMemo(() => { const counts: Record = {}; for (const c of calls) { const s = c.callbackStatus ?? 'PENDING_CALLBACK'; counts[s] = (counts[s] ?? 0) + 1; } return counts; }, [calls]); const filtered = useMemo(() => { let rows = calls; if (tab === 'PENDING_CALLBACK') rows = rows.filter(c => c.callbackStatus === 'PENDING_CALLBACK' || !c.callbackStatus); else if (tab === 'CALLBACK_ATTEMPTED') rows = rows.filter(c => c.callbackStatus === 'CALLBACK_ATTEMPTED'); else if (tab === 'CALLBACK_COMPLETED') rows = rows.filter(c => c.callbackStatus === 'CALLBACK_COMPLETED' || c.callbackStatus === 'WRONG_NUMBER'); if (search.trim()) { const q = search.toLowerCase(); rows = rows.filter(c => (c.callerNumber?.primaryPhoneNumber ?? '').includes(q) || (c.agentName ?? '').toLowerCase().includes(q) || (c.callSourceNumber ?? '').toLowerCase().includes(q), ); } if (sortDescriptor.column) { const dir = sortDescriptor.direction === 'ascending' ? 1 : -1; rows = [...rows].sort((a, b) => { switch (sortDescriptor.column) { 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 'count': return ((a.missedCallCount ?? 1) - (b.missedCallCount ?? 1)) * dir; case 'sla': return ((a.sla ?? 0) - (b.sla ?? 0)) * dir; case 'agent': return (a.agentName ?? '').localeCompare(b.agentName ?? '') * dir; default: return 0; } }); } return rows; }, [calls, tab, 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); }; const handleTab = (key: StatusTab) => { setTab(key); setCurrentPage(1); }; const tabItems = [ { id: 'all' as const, label: 'All', badge: calls.length > 0 ? String(calls.length) : undefined }, { id: 'PENDING_CALLBACK' as const, label: 'Pending', badge: statusCounts.PENDING_CALLBACK ? String(statusCounts.PENDING_CALLBACK) : undefined }, { id: 'CALLBACK_ATTEMPTED' as const, label: 'Attempted', badge: statusCounts.CALLBACK_ATTEMPTED ? String(statusCounts.CALLBACK_ATTEMPTED) : undefined }, { id: 'CALLBACK_COMPLETED' as const, label: 'Completed', badge: (statusCounts.CALLBACK_COMPLETED || statusCounts.WRONG_NUMBER) ? String((statusCounts.CALLBACK_COMPLETED ?? 0) + (statusCounts.WRONG_NUMBER ?? 0)) : undefined }, ]; return ( <>
{/* Tabs + toolbar */}
handleTab(key as StatusTab)}> {(item) => }
{/* Table */}
{loading ? (

Loading missed calls...

) : filtered.length === 0 ? (

{search ? 'No matching calls' : 'No missed calls'}

) : (
{visibleColumns.has('caller') && } {visibleColumns.has('dateTime') && } {visibleColumns.has('branch') && } {visibleColumns.has('agent') && } {visibleColumns.has('count') && } {visibleColumns.has('status') && } {visibleColumns.has('sla') && } {visibleColumns.has('callback') && } {(call) => { const phone = call.callerNumber?.primaryPhoneNumber ?? ''; const status = call.callbackStatus ?? 'PENDING_CALLBACK'; return ( {visibleColumns.has('caller') && ( {phone ? ( ) : Unknown} )} {visibleColumns.has('dateTime') && ( {call.startedAt ? (
{formatDateOrdinal(call.startedAt)} {formatTimeOnly(call.startedAt)}
) : }
)} {visibleColumns.has('branch') && ( {call.callSourceNumber || '—'} )} {visibleColumns.has('agent') && ( {call.agentName || '—'} )} {visibleColumns.has('count') && ( {call.missedCallCount && call.missedCallCount > 1 ? ( {call.missedCallCount}x ) : 1} )} {visibleColumns.has('status') && ( {STATUS_LABELS[status] ?? status} )} {visibleColumns.has('sla') && ( {call.sla != null ? (() => { const status = computeSlaStatus(call.sla); return ( {call.sla}% ); })() : } )} {visibleColumns.has('callback') && ( {call.callbackAttemptedAt ? (
{formatDateOrdinal(call.callbackAttemptedAt)} {formatTimeOnly(call.callbackAttemptedAt)}
) : }
)}
); }}
)}
{/* Pagination */} {totalPages > 1 && (
)}
); };