import { useEffect, useMemo, useState } from 'react'; import { faMagnifyingGlass } from '@fortawesome/pro-duotone-svg-icons'; 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 { TopBar } from '@/components/layout/top-bar'; import { PhoneActionCell } from '@/components/call-desk/phone-action-cell'; import { apiClient } from '@/lib/api-client'; import { formatPhone, formatDateTimeShort } from '@/lib/format'; 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; }; 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 } } } }`; const formatDate = (iso: string): string => formatDateTimeShort(iso); const computeSla = (dateStr: string): { label: string; color: 'success' | 'warning' | 'error' } => { const minutes = Math.max(0, Math.round((Date.now() - new Date(dateStr).getTime()) / 60000)); if (minutes < 15) return { label: `${minutes}m`, color: 'success' }; if (minutes < 30) return { label: `${minutes}m`, color: 'warning' }; if (minutes < 60) return { label: `${minutes}m`, color: 'error' }; const hours = Math.floor(minutes / 60); if (hours < 24) return { label: `${hours}h ${minutes % 60}m`, color: 'error' }; return { label: `${Math.floor(hours / 24)}d`, color: 'error' }; }; 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', }; export const MissedCallsPage = () => { const [calls, setCalls] = useState([]); const [loading, setLoading] = useState(true); const [tab, setTab] = useState('all'); const [search, setSearch] = useState(''); useEffect(() => { 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)); }, []); 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), ); } return rows; }, [calls, tab, search]); 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 ( <>
setTab(key as StatusTab)}> {(item) => }
{loading ? (

Loading missed calls...

) : filtered.length === 0 ? (

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

) : ( {(call) => { const phone = call.callerNumber?.primaryPhoneNumber ?? ''; const status = call.callbackstatus ?? 'PENDING_CALLBACK'; const sla = call.startedAt ? computeSla(call.startedAt) : null; return ( {phone ? ( ) : Unknown} {call.startedAt ? formatDate(call.startedAt) : '—'} {call.callsourcenumber || '—'} {call.agentName || '—'} {call.missedcallcount && call.missedcallcount > 1 ? ( {call.missedcallcount}x ) : 1} {STATUS_LABELS[status] ?? status} {sla && {sla.label}} ); }}
)}
); };