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:
@@ -8,8 +8,9 @@ import { useData } from '@/providers/data-provider';
|
||||
import { useWorklist } from '@/hooks/use-worklist';
|
||||
import { useSip } from '@/providers/sip-provider';
|
||||
import { WorklistPanel } from '@/components/call-desk/worklist-panel';
|
||||
import type { WorklistLead } from '@/components/call-desk/worklist-panel';
|
||||
import type { WorklistSelection } from '@/components/call-desk/worklist-panel';
|
||||
import { ContextPanel } from '@/components/call-desk/context-panel';
|
||||
import type { ContextPanelSubject } from '@/components/call-desk/context-panel';
|
||||
import { ActiveCallCard } from '@/components/call-desk/active-call-card';
|
||||
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
@@ -21,7 +22,8 @@ export const CallDeskPage = () => {
|
||||
const { leadActivities, calls, followUps: dataFollowUps, patients, appointments } = useData();
|
||||
const { callState, callerNumber, callUcid, dialOutbound, isRegistered } = useSip();
|
||||
const { missedCalls, followUps, marketingLeads, loading } = useWorklist();
|
||||
const [selectedLead, setSelectedLead] = useState<WorklistLead | null>(null);
|
||||
const [selectedLead, setSelectedLead] = useState<ContextPanelSubject | null>(null);
|
||||
const [selectedItemId, setSelectedItemId] = useState<string | null>(null);
|
||||
const [contextOpen, setContextOpen] = useState(true);
|
||||
const [activeMissedCallId, setActiveMissedCallId] = useState<string | null>(null);
|
||||
const [callDismissed, setCallDismissed] = useState(false);
|
||||
@@ -134,6 +136,34 @@ export const CallDeskPage = () => {
|
||||
: selectedLead;
|
||||
const activeLeadFull = activeLead as any;
|
||||
|
||||
// Handle selection from any worklist row type. Leads use the lead
|
||||
// object directly; missed calls and follow-ups build a synthetic
|
||||
// lead-like object from their phone/patientId so the P360 context
|
||||
// panel can render for any row type.
|
||||
const handleSelectItem = useCallback((selection: WorklistSelection) => {
|
||||
setSelectedItemId(selection.rowId);
|
||||
|
||||
if (selection.lead) {
|
||||
// Lead row — use the full lead object as before
|
||||
setSelectedLead(selection.lead);
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-lead row (missed call, follow-up, callback) — build a
|
||||
// ContextPanelSubject from the row's available data. The panel
|
||||
// uses contactPhone for call-history matching and patientId for
|
||||
// appointment/follow-up lookups. No type cast needed — the
|
||||
// ContextPanelSubject type accepts these optional fields.
|
||||
const phone = selection.phoneRaw ? selection.phoneRaw.replace(/\D/g, '').slice(-10) : '';
|
||||
const subject: ContextPanelSubject = {
|
||||
id: selection.leadId ?? selection.rowId,
|
||||
contactName: { firstName: selection.name.split(' ')[0] || '', lastName: selection.name.split(' ').slice(1).join(' ') || '' },
|
||||
contactPhone: phone ? [{ number: phone, callingCode: '+91' }] : [],
|
||||
patientId: selection.patientId,
|
||||
};
|
||||
setSelectedLead(subject);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{/* Compact header: title + name on left, status + toggle on right */}
|
||||
@@ -250,8 +280,8 @@ export const CallDeskPage = () => {
|
||||
followUps={followUps}
|
||||
leads={marketingLeads}
|
||||
loading={loading}
|
||||
onSelectLead={(lead) => setSelectedLead(lead)}
|
||||
selectedLeadId={selectedLead?.id ?? null}
|
||||
onSelectItem={handleSelectItem}
|
||||
selectedItemId={selectedItemId}
|
||||
onDialMissedCall={(id) => setActiveMissedCallId(id)}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user