mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
feat: Contacts page + P360 for all tabs + dynamic column toggle + slot flicker fix
Contacts page:
- New /contacts route — shows leads with source=PHONE/WALK_IN/REFERRAL
- Leads page now excludes those sources (campaign-sourced only)
- Sidebar: Contacts nav item added for all roles; Leads added for cc-agent
- Same LeadTable + pagination + CSV export pattern as All Leads
P360 context panel for all worklist tabs (#6-10):
- WorklistPanel: onSelectLead → onSelectItem (generic WorklistSelection)
- call-desk: handleSelectItem builds ContextPanelSubject for any row type
- ContextPanelSubject type replaces (lead as any).patientId casts
- Highlight tracks row.id (mc-*/fu-*/lead-*) not lead.id
Dynamic column toggle (blank-screen fix):
- missed-calls + call-recordings refactored to React Aria dynamic
collections API (Table.Header columns={} + Table.Row columns={})
- Fixes "Cell count must match column count" crash on column hide
- Row-header column metadata in columnDefs instead of hardcoded JSX
Slot flickering fix (#2):
- Removed clinic + timeSlot from slot-fetch useEffect deps (circular
loop: effect sets clinic → clinic in deps → re-fires)
- Memoized timeSlotSelectItems
Other:
- GlobalSearch hidden (stale appointment state on navigation)
- Branch column: shows campaign name from relation, falls back to DID
- formatSource maps PHONE → "Phone"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ type WorklistFollowUp = {
|
||||
followUpStatus: string | null;
|
||||
scheduledAt: string | null;
|
||||
priority: string | null;
|
||||
patientId?: string | null;
|
||||
patientName?: string;
|
||||
patientPhone?: string;
|
||||
};
|
||||
@@ -53,17 +54,30 @@ type MissedCall = {
|
||||
callSourceNumber: string | null;
|
||||
missedCallCount: number | null;
|
||||
callbackAttemptedAt: string | null;
|
||||
campaign?: { id: string; campaignName: string } | null;
|
||||
};
|
||||
|
||||
type MissedSubTab = 'pending' | 'attempted' | 'completed' | 'invalid';
|
||||
|
||||
// Generic selection from any worklist row — the call-desk resolves
|
||||
// lead/patient context from whatever is available on the row.
|
||||
export type WorklistSelection = {
|
||||
rowId: string;
|
||||
type: 'missed' | 'callback' | 'follow-up' | 'lead';
|
||||
lead: WorklistLead | null;
|
||||
phoneRaw: string | null;
|
||||
patientId: string | null;
|
||||
leadId: string | null;
|
||||
name: string;
|
||||
};
|
||||
|
||||
interface WorklistPanelProps {
|
||||
missedCalls: MissedCall[];
|
||||
followUps: WorklistFollowUp[];
|
||||
leads: WorklistLead[];
|
||||
loading: boolean;
|
||||
onSelectLead: (lead: WorklistLead) => void;
|
||||
selectedLeadId: string | null;
|
||||
onSelectItem: (selection: WorklistSelection) => void;
|
||||
selectedItemId: string | null;
|
||||
onDialMissedCall?: (missedCallId: string) => void;
|
||||
}
|
||||
|
||||
@@ -82,6 +96,7 @@ type WorklistRow = {
|
||||
createdAt: string;
|
||||
taskState: 'PENDING' | 'ATTEMPTED' | 'SCHEDULED';
|
||||
leadId: string | null;
|
||||
patientId: string | null;
|
||||
originalLead: WorklistLead | null;
|
||||
lastContactedAt: string | null;
|
||||
contactAttempts: number;
|
||||
@@ -171,10 +186,27 @@ const formatSource = (source: string): string => {
|
||||
REFERRAL: 'Referral',
|
||||
WEBSITE: 'Website',
|
||||
PHONE_INQUIRY: 'Phone',
|
||||
PHONE: 'Phone',
|
||||
OTHER: 'Other',
|
||||
};
|
||||
return map[source] ?? source.replace(/_/g, ' ');
|
||||
};
|
||||
|
||||
// Resolve a DID (e.g. "918041763265") to a friendly branch/campaign name.
|
||||
// The DID is the phone number the caller dialed — it identifies which
|
||||
// hospital branch or campaign the call came through. Falls back to the
|
||||
// last 10 digits if no mapping is found.
|
||||
const formatDid = (did: string): string => {
|
||||
if (!did) return '—';
|
||||
// Known DIDs — loaded from the sidecar theme tokens at boot (via
|
||||
// the ThemeTokenProvider). For now, hardcode nothing — strip country
|
||||
// code and show the DID as a short number so it's at least readable.
|
||||
const digits = did.replace(/\D/g, '');
|
||||
// Strip leading 91 (India) for display
|
||||
const short = digits.length > 10 && digits.startsWith('91') ? digits.slice(2) : digits;
|
||||
return short;
|
||||
};
|
||||
|
||||
const IconInbound = faIcon(faPhoneArrowDown);
|
||||
const IconOutbound = faIcon(faPhoneArrowUp);
|
||||
|
||||
@@ -200,10 +232,14 @@ const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], lea
|
||||
createdAt: call.createdAt,
|
||||
taskState: call.callbackStatus === 'CALLBACK_ATTEMPTED' ? 'ATTEMPTED' : 'PENDING',
|
||||
leadId: call.leadId,
|
||||
patientId: (call as any).patientId ?? null,
|
||||
originalLead: null,
|
||||
lastContactedAt: call.callbackAttemptedAt ?? call.startedAt ?? call.createdAt,
|
||||
contactAttempts: 0,
|
||||
source: call.callSourceNumber ?? null,
|
||||
// Branch column: prefer the campaign name (e.g. "Cervical Cancer
|
||||
// Screening Drive") over the raw DID. Falls back to formatted DID
|
||||
// for organic calls with no campaign.
|
||||
source: call.campaign?.campaignName ?? (call.callSourceNumber ? formatDid(call.callSourceNumber) : null),
|
||||
lastDisposition: call.disposition ?? null,
|
||||
missedCallId: call.id,
|
||||
});
|
||||
@@ -234,6 +270,7 @@ const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], lea
|
||||
createdAt: fu.createdAt ?? fu.scheduledAt ?? new Date().toISOString(),
|
||||
taskState: isOverdue ? 'PENDING' : (fu.followUpStatus === 'COMPLETED' ? 'ATTEMPTED' : 'SCHEDULED'),
|
||||
leadId: null,
|
||||
patientId: fu.patientId ?? null,
|
||||
originalLead: null,
|
||||
lastContactedAt: fu.scheduledAt ?? fu.createdAt ?? null,
|
||||
contactAttempts: 0,
|
||||
@@ -261,6 +298,7 @@ const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], lea
|
||||
createdAt: lead.createdAt,
|
||||
taskState: 'PENDING',
|
||||
leadId: lead.id,
|
||||
patientId: (lead as any).patientId ?? null,
|
||||
originalLead: lead,
|
||||
lastContactedAt: lead.lastContacted ?? null,
|
||||
contactAttempts: lead.contactAttempts ?? 0,
|
||||
@@ -286,7 +324,7 @@ const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], lea
|
||||
return actionableRows;
|
||||
};
|
||||
|
||||
export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelectLead, selectedLeadId, onDialMissedCall }: WorklistPanelProps) => {
|
||||
export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelectItem, selectedItemId, onDialMissedCall }: WorklistPanelProps) => {
|
||||
const [tab, setTab] = useState<TabKey>('all');
|
||||
const [search, setSearch] = useState('');
|
||||
// Missed tab always shows PENDING callbacks. Attempted/Completed/Invalid
|
||||
@@ -466,7 +504,7 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
|
||||
{(row) => {
|
||||
const priority = priorityConfig[row.priority] ?? priorityConfig.NORMAL;
|
||||
const sla = computeSla(row);
|
||||
const isSelected = row.originalLead !== null && row.originalLead.id === selectedLeadId;
|
||||
const isSelected = row.id === selectedItemId;
|
||||
|
||||
// Sub-line: last interaction context
|
||||
const subLine = row.lastContactedAt
|
||||
@@ -481,7 +519,15 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
|
||||
isSelected && 'bg-brand-primary',
|
||||
)}
|
||||
onAction={() => {
|
||||
if (row.originalLead) onSelectLead(row.originalLead);
|
||||
onSelectItem({
|
||||
rowId: row.id,
|
||||
type: row.type,
|
||||
lead: row.originalLead,
|
||||
phoneRaw: row.phoneRaw || null,
|
||||
patientId: row.patientId,
|
||||
leadId: row.leadId,
|
||||
name: row.name,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Table.Cell>
|
||||
|
||||
Reference in New Issue
Block a user