feat: worklist sorting, contextual disposition, context panel redesign, notifications

- Worklist default sort descending (newest first), sortable column headers (PRIORITY, PATIENT, SLA) via React Aria
- Contextual disposition: auto-selects based on in-call actions (appointment → APPOINTMENT_BOOKED, enquiry → INFO_PROVIDED, transfer → FOLLOW_UP_SCHEDULED)
- Context panel redesign: collapsible AI Insight, Upcoming (appointments + follow-ups + linked patient), Recent (calls + activities) sections; auto-collapse on AI chat start
- Appointments added to DataProvider with APPOINTMENTS_QUERY, Appointment type, transform
- Notification bell for admin/supervisor: performance alerts (idle time, NPS, conversion thresholds) with toast on load + bell dropdown with dismiss; demo alerts as fallback
- Slideout z-index fix: added z-50 to slideout ModalOverlay matching modal component

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 14:45:52 +05:30
parent 0477064b3e
commit c3c3f4b3d7
18 changed files with 882 additions and 389 deletions

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { faPhoneArrowDown, faPhoneArrowUp, faMagnifyingGlass } from '@fortawesome/pro-duotone-svg-icons';
import type { SortDescriptor } from 'react-aria-components';
import { faIcon } from '@/lib/icon-wrapper';
import { Table } from '@/components/application/table/table';
@@ -60,6 +61,7 @@ interface WorklistPanelProps {
loading: boolean;
onSelectLead: (lead: WorklistLead) => void;
selectedLeadId: string | null;
onDialMissedCall?: (missedCallId: string) => void;
}
type TabKey = 'all' | 'missed' | 'leads' | 'follow-ups';
@@ -82,6 +84,7 @@ type WorklistRow = {
contactAttempts: number;
source: string | null;
lastDisposition: string | null;
missedCallId: string | null;
};
const priorityConfig: Record<string, { color: 'error' | 'warning' | 'brand' | 'gray'; label: string; sort: number }> = {
@@ -164,6 +167,7 @@ const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], lea
contactAttempts: 0,
source: call.callsourcenumber ?? null,
lastDisposition: call.disposition ?? null,
missedCallId: call.id,
});
}
@@ -190,6 +194,7 @@ const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], lea
contactAttempts: 0,
source: null,
lastDisposition: null,
missedCallId: null,
});
}
@@ -216,6 +221,7 @@ const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], lea
contactAttempts: lead.contactAttempts ?? 0,
source: lead.leadSource ?? lead.utmCampaign ?? null,
lastDisposition: null,
missedCallId: null,
});
}
@@ -226,16 +232,17 @@ const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], lea
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 new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
});
return actionableRows;
};
export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelectLead, selectedLeadId }: WorklistPanelProps) => {
export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelectLead, selectedLeadId, onDialMissedCall }: WorklistPanelProps) => {
const [tab, setTab] = useState<TabKey>('all');
const [search, setSearch] = useState('');
const [missedSubTab, setMissedSubTab] = useState<MissedSubTab>('pending');
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({ column: 'sla', direction: 'descending' });
const missedByStatus = useMemo(() => ({
pending: missedCalls.filter(c => c.callbackstatus === 'PENDING_CALLBACK' || !c.callbackstatus),
@@ -268,8 +275,30 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
);
}
if (sortDescriptor.column) {
const dir = sortDescriptor.direction === 'ascending' ? 1 : -1;
rows = [...rows].sort((a, b) => {
switch (sortDescriptor.column) {
case 'priority': {
const pa = priorityConfig[a.priority]?.sort ?? 2;
const pb = priorityConfig[b.priority]?.sort ?? 2;
return (pa - pb) * dir;
}
case 'name':
return a.name.localeCompare(b.name) * dir;
case 'sla': {
const ta = new Date(a.lastContactedAt ?? a.createdAt).getTime();
const tb = new Date(b.lastContactedAt ?? b.createdAt).getTime();
return (ta - tb) * dir;
}
default:
return 0;
}
});
}
return rows;
}, [allRows, tab, search]);
}, [allRows, tab, search, sortDescriptor, missedSubTabRows]);
const missedCount = allRows.filter((r) => r.type === 'missed').length;
const leadCount = allRows.filter((r) => r.type === 'lead').length;
@@ -373,13 +402,13 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
</div>
) : (
<div className="flex flex-1 flex-col min-h-0 overflow-hidden px-2 pt-3">
<Table size="sm">
<Table size="sm" sortDescriptor={sortDescriptor} onSortChange={setSortDescriptor}>
<Table.Header>
<Table.Head label="PRIORITY" className="w-20" isRowHeader />
<Table.Head label="PATIENT" />
<Table.Head id="priority" label="PRIORITY" className="w-20" isRowHeader allowsSorting />
<Table.Head id="name" label="PATIENT" allowsSorting />
<Table.Head label="PHONE" />
<Table.Head label={tab === 'missed' ? 'BRANCH' : 'SOURCE'} className="w-28" />
<Table.Head label="SLA" className="w-24" />
<Table.Head id="sla" label="SLA" className="w-24" allowsSorting />
</Table.Header>
<Table.Body items={pagedRows}>
{(row) => {
@@ -432,6 +461,7 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
phoneNumber={row.phoneRaw}
displayNumber={row.phone}
leadId={row.leadId ?? undefined}
onDial={row.missedCallId ? () => onDialMissedCall?.(row.missedCallId!) : undefined}
/>
) : (
<span className="text-xs text-quaternary italic">No phone</span>