import { useCallback, useMemo, useState } from 'react'; import type { FC, HTMLAttributes } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPhoneArrowDown, faPhoneArrowUp, } from '@fortawesome/pro-duotone-svg-icons'; import { SearchLg } from '@untitledui/icons'; import { Table } from '@/components/application/table/table'; import { Badge } from '@/components/base/badges/badges'; import { Input } from '@/components/base/input/input'; import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs'; import { ClickToCallButton } from './click-to-call-button'; import { formatPhone } from '@/lib/format'; import { cx } from '@/utils/cx'; type WorklistLead = { id: string; createdAt: string; contactName: { firstName: string; lastName: string } | null; contactPhone: { number: string; callingCode: string }[] | null; leadSource: string | null; leadStatus: string | null; interestedService: string | null; aiSummary: string | null; aiSuggestedAction: string | null; }; type WorklistFollowUp = { id: string; createdAt: string | null; followUpType: string | null; followUpStatus: string | null; scheduledAt: string | null; priority: string | null; }; type MissedCall = { id: string; createdAt: string; callDirection: string | null; callerNumber: { number: string; callingCode: string }[] | null; startedAt: string | null; leadId: string | null; }; interface WorklistPanelProps { missedCalls: MissedCall[]; followUps: WorklistFollowUp[]; leads: WorklistLead[]; loading: boolean; onSelectLead: (lead: WorklistLead) => void; selectedLeadId: string | null; } type TabKey = 'all' | 'missed' | 'callbacks' | 'follow-ups'; // Unified row type for the table type WorklistRow = { id: string; type: 'missed' | 'callback' | 'follow-up' | 'lead'; priority: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW'; name: string; phone: string; phoneRaw: string; direction: 'inbound' | 'outbound' | null; typeLabel: string; reason: string; createdAt: string; taskState: 'PENDING' | 'ATTEMPTED' | 'SCHEDULED'; leadId: string | null; originalLead: WorklistLead | null; }; const priorityConfig: Record = { URGENT: { color: 'error', label: 'Urgent', sort: 0 }, HIGH: { color: 'warning', label: 'High', sort: 1 }, NORMAL: { color: 'brand', label: 'Normal', sort: 2 }, LOW: { color: 'gray', label: 'Low', sort: 3 }, }; const followUpLabel: Record = { CALLBACK: 'Callback', APPOINTMENT_REMINDER: 'Appt Reminder', POST_VISIT: 'Post-visit', MARKETING: 'Marketing', REVIEW_REQUEST: 'Review', }; // Compute SLA: minutes since created, color-coded const computeSla = (createdAt: string): { label: string; color: 'success' | 'warning' | 'error' } => { const minutes = Math.max(0, Math.round((Date.now() - new Date(createdAt).getTime()) / 60000)); if (minutes < 1) return { label: '<1m', color: 'success' }; 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 IconInbound: FC> = ({ className }) => ( ); const IconOutbound: FC> = ({ className }) => ( ); const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], leads: WorklistLead[]): WorklistRow[] => { const rows: WorklistRow[] = []; for (const call of missedCalls) { const phone = call.callerNumber?.[0]; rows.push({ id: `mc-${call.id}`, type: 'missed', priority: 'HIGH', name: phone ? formatPhone(phone) : 'Unknown', phone: phone ? formatPhone(phone) : '', phoneRaw: phone?.number ?? '', direction: call.callDirection === 'OUTBOUND' ? 'outbound' : 'inbound', typeLabel: 'Missed Call', reason: call.startedAt ? `Missed at ${new Date(call.startedAt).toLocaleTimeString('en-IN', { hour: 'numeric', minute: '2-digit', hour12: true })}` : 'Missed call', createdAt: call.createdAt, taskState: 'PENDING', leadId: call.leadId, originalLead: null, }); } for (const fu of followUps) { const isOverdue = fu.followUpStatus === 'OVERDUE' || (fu.scheduledAt !== null && new Date(fu.scheduledAt) < new Date()); const label = followUpLabel[fu.followUpType ?? ''] ?? fu.followUpType ?? 'Follow-up'; rows.push({ id: `fu-${fu.id}`, type: fu.followUpType === 'CALLBACK' ? 'callback' : 'follow-up', priority: (fu.priority as WorklistRow['priority']) ?? (isOverdue ? 'HIGH' : 'NORMAL'), name: label, phone: '', phoneRaw: '', direction: null, typeLabel: fu.followUpType === 'CALLBACK' ? 'Callback' : 'Follow-up', reason: fu.scheduledAt ? `Scheduled ${new Date(fu.scheduledAt).toLocaleString('en-IN', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true })}` : '', createdAt: fu.createdAt ?? fu.scheduledAt ?? new Date().toISOString(), taskState: isOverdue ? 'PENDING' : (fu.followUpStatus === 'COMPLETED' ? 'ATTEMPTED' : 'SCHEDULED'), leadId: null, originalLead: null, }); } for (const lead of leads) { const firstName = lead.contactName?.firstName ?? ''; const lastName = lead.contactName?.lastName ?? ''; const fullName = `${firstName} ${lastName}`.trim() || 'Unknown'; const phone = lead.contactPhone?.[0]; rows.push({ id: `lead-${lead.id}`, type: 'lead', priority: 'NORMAL', name: fullName, phone: phone ? formatPhone(phone) : '', phoneRaw: phone?.number ?? '', direction: null, typeLabel: 'Lead', reason: lead.interestedService ?? lead.aiSuggestedAction ?? '', createdAt: lead.createdAt, taskState: 'PENDING', leadId: lead.id, originalLead: lead, }); } // Sort by priority (urgent first), then by creation time (oldest first) rows.sort((a, b) => { const pa = priorityConfig[a.priority]?.sort ?? 2; const pb = priorityConfig[b.priority]?.sort ?? 2; if (pa !== pb) return pa - pb; return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); }); return rows; }; const typeConfig: Record = { missed: { color: 'error' }, callback: { color: 'brand' }, 'follow-up': { color: 'blue-light' }, lead: { color: 'gray' }, }; export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelectLead, selectedLeadId }: WorklistPanelProps) => { const [tab, setTab] = useState('all'); const [search, setSearch] = useState(''); const allRows = useMemo( () => buildRows(missedCalls, followUps, leads), [missedCalls, followUps, leads], ); const filteredRows = useMemo(() => { let rows = allRows; // Tab filter if (tab === 'missed') rows = rows.filter((r) => r.type === 'missed'); else if (tab === 'callbacks') rows = rows.filter((r) => r.type === 'callback'); else if (tab === 'follow-ups') rows = rows.filter((r) => r.type === 'follow-up'); // Search filter if (search.trim()) { const q = search.toLowerCase(); rows = rows.filter( (r) => r.name.toLowerCase().includes(q) || r.phone.toLowerCase().includes(q) || r.phoneRaw.includes(q), ); } return rows; }, [allRows, tab, search]); const missedCount = allRows.filter((r) => r.type === 'missed').length; const callbackCount = allRows.filter((r) => r.type === 'callback').length; const followUpCount = allRows.filter((r) => r.type === 'follow-up').length; const PAGE_SIZE = 15; const [page, setPage] = useState(1); // Reset page when filters change const handleTabChange = useCallback((key: TabKey) => { setTab(key); setPage(1); }, []); const handleSearch = useCallback((value: string) => { setSearch(value); setPage(1); }, []); const totalPages = Math.max(1, Math.ceil(filteredRows.length / PAGE_SIZE)); const pagedRows = filteredRows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); const tabItems = [ { id: 'all' as const, label: 'All Tasks', badge: allRows.length > 0 ? String(allRows.length) : undefined }, { id: 'missed' as const, label: 'Missed Calls', badge: missedCount > 0 ? String(missedCount) : undefined }, { id: 'callbacks' as const, label: 'Callbacks', badge: callbackCount > 0 ? String(callbackCount) : undefined }, { id: 'follow-ups' as const, label: 'Follow-ups', badge: followUpCount > 0 ? String(followUpCount) : undefined }, ]; if (loading) { return (

Loading worklist...

); } const isEmpty = missedCalls.length === 0 && followUps.length === 0 && leads.length === 0; if (isEmpty) { return (

All clear

No pending items in your worklist

); } return (
{/* Filter tabs + search — single row */}
handleTabChange(key as TabKey)}> {(item) => }
{filteredRows.length === 0 ? (

{search ? 'No matching items' : 'No items in this category'}

) : (
{(row) => { const priority = priorityConfig[row.priority] ?? priorityConfig.NORMAL; const sla = computeSla(row.createdAt); const typeCfg = typeConfig[row.type]; const isSelected = row.originalLead !== null && row.originalLead.id === selectedLeadId; return ( { if (row.originalLead) { onSelectLead(row.originalLead); } }} > {priority.label}
{row.direction === 'inbound' && ( )} {row.direction === 'outbound' && ( )} {row.name}
{row.phone || '\u2014'} {row.typeLabel} {sla.label}
{row.phoneRaw ? ( ) : ( No phone )}
); }}
{totalPages > 1 && (
Showing {(page - 1) * PAGE_SIZE + 1}–{Math.min(page * PAGE_SIZE, filteredRows.length)} of {filteredRows.length}
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => ( ))}
)}
)}
); }; export type { WorklistLead };