mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
feat: call-desk refresh — disposition modal, active-call UI, worklist + perf updates
- Call-desk: active-call-card supervisor presence badges, incoming-call-card polish, transfer-dialog, call-log - Disposition modal: auto-lock based on actions taken, not-interested split - Forms: appointment-form + enquiry-form improvements (placeholder handling, phone format) - Worklist-panel: pagination awareness, filter chips - Pages: all-leads/patients/patient-360/missed-calls/team-performance/call-history/appointments polish - SIP: sip-client reconnect, sip-provider + sip-manager state, agent-status-toggle spinner - Hooks: use-agent-state supervisor SSE events, use-worklist, use-performance-alerts - Types: entities.ts extended Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom } from '@/state/
|
||||
import { setOutboundPending } from '@/state/sip-manager';
|
||||
import { useSip } from '@/providers/sip-provider';
|
||||
import { DispositionModal } from './disposition-modal';
|
||||
import type { CallAction } from './disposition-modal';
|
||||
import { AppointmentForm } from './appointment-form';
|
||||
import { TransferDialog } from './transfer-dialog';
|
||||
import { EnquiryForm } from './enquiry-form';
|
||||
@@ -48,7 +49,18 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
const [enquiryOpen, setEnquiryOpen] = useState(false);
|
||||
const [dispositionOpen, setDispositionOpen] = useState(false);
|
||||
const [callerDisconnected, setCallerDisconnected] = useState(false);
|
||||
const [suggestedDisposition, setSuggestedDisposition] = useState<CallDisposition | null>(null);
|
||||
// Actions actually recorded during this call. Drives the disposition
|
||||
// modal's priority-lock: if the agent booked an appointment and logged
|
||||
// an enquiry, both badges render and the primary disposition is
|
||||
// locked to APPOINTMENT_BOOKED.
|
||||
const [actionsTaken, setActionsTaken] = useState<CallAction[]>([]);
|
||||
const addActions = (...newActions: CallAction[]) => {
|
||||
setActionsTaken((prev) => {
|
||||
const next = new Set(prev);
|
||||
for (const a of newActions) next.add(a);
|
||||
return Array.from(next);
|
||||
});
|
||||
};
|
||||
|
||||
const agentConfig = localStorage.getItem('helix_agent_config');
|
||||
const agentIdForState = agentConfig ? (() => { try { return JSON.parse(agentConfig).ozonetelAgentId; } catch { return null; } })() : null;
|
||||
@@ -104,6 +116,7 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
direction: callDirectionRef.current,
|
||||
durationSec: callDuration,
|
||||
leadId: lead?.id ?? null,
|
||||
leadName: fullName || null,
|
||||
notes,
|
||||
missedCallId: missedCallId ?? undefined,
|
||||
};
|
||||
@@ -115,24 +128,9 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
console.warn('[DISPOSE] No callUcid — skipping disposition');
|
||||
}
|
||||
|
||||
// Side effects
|
||||
if (disposition === 'FOLLOW_UP_SCHEDULED') {
|
||||
try {
|
||||
await apiClient.graphql(`mutation($data: FollowUpCreateInput!) { createFollowUp(data: $data) { id } }`, {
|
||||
data: {
|
||||
name: `Follow-up — ${fullName || phoneDisplay}`,
|
||||
typeCustom: 'CALLBACK',
|
||||
status: 'PENDING',
|
||||
assignedAgent: null,
|
||||
priority: 'NORMAL',
|
||||
scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
},
|
||||
}, { silent: true });
|
||||
notify.success('Follow-up Created', 'Callback scheduled for tomorrow');
|
||||
} catch {
|
||||
notify.info('Follow-up', 'Could not auto-create follow-up');
|
||||
}
|
||||
}
|
||||
// Follow-ups are created by the enquiry form (where the agent picks
|
||||
// the date + context). No second creation here — that was causing
|
||||
// duplicate entries on every FOLLOW_UP_SCHEDULED call.
|
||||
|
||||
// Clear persisted UCID — disposition was submitted, no need for sendBeacon fallback
|
||||
localStorage.removeItem('helix_active_ucid');
|
||||
@@ -141,15 +139,24 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
handleReset();
|
||||
};
|
||||
|
||||
const handleAppointmentSaved = () => {
|
||||
const handleAppointmentSaved = (outcome: 'BOOKED' | 'RESCHEDULED' | 'CANCELLED') => {
|
||||
setAppointmentOpen(false);
|
||||
setSuggestedDisposition('APPOINTMENT_BOOKED');
|
||||
notify.success('Appointment Booked', 'Payment link will be sent to the patient');
|
||||
if (outcome === 'RESCHEDULED') {
|
||||
addActions('RESCHEDULE');
|
||||
notify.success('Appointment Rescheduled');
|
||||
} else if (outcome === 'CANCELLED') {
|
||||
addActions('CANCEL');
|
||||
notify.success('Appointment Cancelled');
|
||||
} else {
|
||||
addActions('APPOINTMENT');
|
||||
notify.success('Appointment Booked', 'Payment link will be sent to the patient');
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setDispositionOpen(false);
|
||||
setCallerDisconnected(false);
|
||||
setActionsTaken([]);
|
||||
setCallState('idle');
|
||||
setCallerNumber(null);
|
||||
setCallUcid(null);
|
||||
@@ -213,7 +220,7 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
return (
|
||||
<div className="rounded-xl border border-secondary bg-primary p-4 text-center">
|
||||
<FontAwesomeIcon icon={faPhoneHangup} className="size-6 text-fg-quaternary mb-2" />
|
||||
<p className="text-sm font-semibold text-primary">Missed Call</p>
|
||||
<p className="text-sm font-semibold text-primary">{fullName || 'Missed Call'}</p>
|
||||
<p className="text-xs text-tertiary mt-1">{phoneDisplay} — not answered</p>
|
||||
<Button size="sm" color="secondary" className="mt-3" onClick={handleReset}>
|
||||
Back to Worklist
|
||||
@@ -317,7 +324,10 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
onClose={() => setTransferOpen(false)}
|
||||
onTransferred={() => {
|
||||
setTransferOpen(false);
|
||||
setSuggestedDisposition('FOLLOW_UP_SCHEDULED');
|
||||
// A transfer implies the original agent handed the call
|
||||
// off — treat that as a follow-up action so the
|
||||
// disposition pre-locks to FOLLOW_UP_SCHEDULED.
|
||||
addActions('FOLLOWUP');
|
||||
setDispositionOpen(true);
|
||||
}}
|
||||
/>
|
||||
@@ -340,10 +350,9 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
leadId={lead?.id ?? null}
|
||||
patientId={(lead as any)?.patientId ?? null}
|
||||
agentName={user.name}
|
||||
onSaved={() => {
|
||||
onSaved={(actions) => {
|
||||
setEnquiryOpen(false);
|
||||
setSuggestedDisposition('INFO_PROVIDED');
|
||||
notify.success('Enquiry Logged');
|
||||
addActions(...actions);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -355,7 +364,13 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
isOpen={dispositionOpen}
|
||||
callerName={fullName || phoneDisplay}
|
||||
callerDisconnected={callerDisconnected}
|
||||
defaultDisposition={suggestedDisposition}
|
||||
// wasAnsweredRef only flips true once callState reaches
|
||||
// 'active'. Outbound callbacks that never connect keep
|
||||
// this false, which narrows the disposition options to
|
||||
// no-answer outcomes and prevents SLA-gaming dispositions
|
||||
// like Info Provided on a call the customer never took.
|
||||
callAnswered={wasAnsweredRef.current}
|
||||
actionsTaken={actionsTaken}
|
||||
onSubmit={handleDisposition}
|
||||
onDismiss={() => {
|
||||
// Agent wants to continue the call — close modal, call stays active
|
||||
|
||||
Reference in New Issue
Block a user