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:
@@ -29,7 +29,10 @@ type AppointmentFormProps = {
|
||||
leadName?: string | null;
|
||||
leadId?: string | null;
|
||||
patientId?: string | null;
|
||||
onSaved?: () => void;
|
||||
// Called after a successful save. Passes back what actually happened so
|
||||
// the parent can pre-lock the disposition (BOOKED vs RESCHEDULED vs
|
||||
// CANCELLED each map to distinct disposition outcomes).
|
||||
onSaved?: (outcome: 'BOOKED' | 'RESCHEDULED' | 'CANCELLED') => void;
|
||||
existingAppointment?: ExistingAppointment | null;
|
||||
};
|
||||
|
||||
@@ -241,7 +244,9 @@ export const AppointmentForm = ({
|
||||
const selectedDoctor = doctors.find(d => d.id === doctor);
|
||||
|
||||
if (isEditMode && existingAppointment) {
|
||||
// Update existing appointment
|
||||
// Update existing appointment. Flip status to RESCHEDULED so
|
||||
// the Appointments > Rescheduled tab reflects it and the
|
||||
// patient timeline records the reschedule event.
|
||||
await apiClient.graphql(
|
||||
`mutation UpdateAppointment($id: UUID!, $data: AppointmentUpdateInput!) {
|
||||
updateAppointment(id: $id, data: $data) { id }
|
||||
@@ -254,9 +259,32 @@ export const AppointmentForm = ({
|
||||
department: selectedDoctor?.department ?? '',
|
||||
doctorId: doctor,
|
||||
reasonForVisit: chiefComplaint || null,
|
||||
status: 'RESCHEDULED',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Propagate name change during reschedule. Same gate as the
|
||||
// create branch — nameChanged implies isNameEditable=true,
|
||||
// which means the agent went through EditPatientConfirmModal.
|
||||
const trimmedName = patientName.trim();
|
||||
const nameChanged = isNameEditable && trimmedName.length > 0 && trimmedName !== initialLeadName;
|
||||
if (nameChanged) {
|
||||
const nameParts = { firstName: trimmedName.split(' ')[0], lastName: trimmedName.split(' ').slice(1).join(' ') || '' };
|
||||
if (patientId) {
|
||||
await apiClient.graphql(
|
||||
`mutation($id: UUID!, $data: PatientUpdateInput!) { updatePatient(id: $id, data: $data) { id } }`,
|
||||
{ id: patientId, data: { fullName: nameParts } },
|
||||
).catch((err: unknown) => console.warn('Failed to update patient name:', err));
|
||||
}
|
||||
if (leadId) {
|
||||
await apiClient.graphql(
|
||||
`mutation($id: UUID!, $data: LeadUpdateInput!) { updateLead(id: $id, data: $data) { id } }`,
|
||||
{ id: leadId, data: { contactName: nameParts } },
|
||||
).catch((err: unknown) => console.warn('Failed to update lead name:', err));
|
||||
}
|
||||
}
|
||||
|
||||
notify.success('Appointment Updated');
|
||||
} else {
|
||||
// If no patient record exists yet (new caller), create one now
|
||||
@@ -271,9 +299,16 @@ export const AppointmentForm = ({
|
||||
const phoneDigits = callerNumber.replace(/\D/g, '').slice(-10);
|
||||
const phoneE164 = `+91${phoneDigits}`;
|
||||
try {
|
||||
const patientData: Record<string, any> = {
|
||||
fullName: nameParts,
|
||||
phones: { primaryPhoneNumber: phoneE164 },
|
||||
patientType: 'NEW',
|
||||
};
|
||||
if (age) patientData.dateOfBirth = new Date(Date.now() - parseInt(age) * 365.25 * 86400000).toISOString().split('T')[0];
|
||||
if (gender) patientData.gender = gender.toUpperCase();
|
||||
const created = await apiClient.graphql<{ createPatient: { id: string } }>(
|
||||
`mutation($data: PatientCreateInput!) { createPatient(data: $data) { id } }`,
|
||||
{ data: { fullName: nameParts, phones: { primaryPhoneNumber: phoneE164 }, patientType: 'NEW' } },
|
||||
{ data: patientData },
|
||||
);
|
||||
resolvedPatientId = created.createPatient.id;
|
||||
} catch (err) {
|
||||
@@ -282,24 +317,26 @@ export const AppointmentForm = ({
|
||||
}
|
||||
|
||||
// Create appointment
|
||||
const appointmentData: Record<string, any> = {
|
||||
scheduledAt,
|
||||
durationMin: 30,
|
||||
appointmentType: 'CONSULTATION',
|
||||
status: 'SCHEDULED',
|
||||
doctorName: selectedDoctor?.name ?? '',
|
||||
department: selectedDoctor?.department ?? '',
|
||||
doctorId: doctor,
|
||||
reasonForVisit: chiefComplaint || null,
|
||||
...(resolvedPatientId ? { patientId: resolvedPatientId } : {}),
|
||||
...(clinic ? { clinicId: clinic } : {}),
|
||||
...(agentNotes ? { agentNotes } : {}),
|
||||
...(source ? { source } : {}),
|
||||
};
|
||||
console.log('[APPOINTMENT] Creating appointment:', JSON.stringify(appointmentData));
|
||||
await apiClient.graphql(
|
||||
`mutation CreateAppointment($data: AppointmentCreateInput!) {
|
||||
createAppointment(data: $data) { id }
|
||||
}`,
|
||||
{
|
||||
data: {
|
||||
scheduledAt,
|
||||
durationMin: 30,
|
||||
appointmentType: 'CONSULTATION',
|
||||
status: 'SCHEDULED',
|
||||
doctorName: selectedDoctor?.name ?? '',
|
||||
department: selectedDoctor?.department ?? '',
|
||||
doctorId: doctor,
|
||||
reasonForVisit: chiefComplaint || null,
|
||||
...(resolvedPatientId ? { patientId: resolvedPatientId } : {}),
|
||||
...(clinic ? { clinicId: clinic } : {}),
|
||||
},
|
||||
},
|
||||
{ data: appointmentData },
|
||||
);
|
||||
|
||||
// Determine whether the agent actually renamed the patient.
|
||||
@@ -309,11 +346,13 @@ export const AppointmentForm = ({
|
||||
const trimmedName = patientName.trim();
|
||||
const nameChanged = isNameEditable && trimmedName.length > 0 && trimmedName !== initialLeadName;
|
||||
|
||||
// Update patient name only when it was empty (new caller with no name).
|
||||
// Don't overwrite an existing patient name — that would
|
||||
// retroactively change the name on all past appointments.
|
||||
// Bug #527: only set name on patients with no existing name.
|
||||
if (nameChanged && patientId && initialLeadName.length === 0) {
|
||||
// Update patient name when the agent explicitly renamed.
|
||||
// `nameChanged` already requires isNameEditable=true (the
|
||||
// agent went through EditPatientConfirmModal), so the
|
||||
// rename intent is unambiguous. Bug #527's silent-overwrite
|
||||
// case can no longer happen because the confirm modal
|
||||
// gates the input.
|
||||
if (nameChanged && patientId) {
|
||||
const nameParts = { firstName: trimmedName.split(' ')[0], lastName: trimmedName.split(' ').slice(1).join(' ') || '' };
|
||||
apiClient.graphql(
|
||||
`mutation($id: UUID!, $data: PatientUpdateInput!) { updatePatient(id: $id, data: $data) { id } }`,
|
||||
@@ -346,21 +385,14 @@ export const AppointmentForm = ({
|
||||
|
||||
// If the agent actually renamed the patient, kick off the
|
||||
// side-effect chain: regenerate the AI summary against the
|
||||
// corrected identity AND invalidate the Redis caller
|
||||
// resolution cache so the next incoming call from this
|
||||
// phone picks up fresh data. Both are fire-and-forget —
|
||||
// the save toast fires immediately either way.
|
||||
// corrected identity. Fire-and-forget; the save toast
|
||||
// fires immediately regardless.
|
||||
if (nameChanged && leadId) {
|
||||
apiClient.post(`/api/lead/${leadId}/enrich`, { phone: callerNumber ?? undefined }, { silent: true }).catch(() => {});
|
||||
} else if (callerNumber) {
|
||||
// No rename but still invalidate the cache so status +
|
||||
// lastContacted updates propagate cleanly to the next
|
||||
// lookup.
|
||||
apiClient.post('/api/caller/invalidate', { phone: callerNumber }, { silent: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
onSaved?.();
|
||||
onSaved?.(isEditMode ? 'RESCHEDULED' : 'BOOKED');
|
||||
} catch (err) {
|
||||
console.error('Failed to save appointment:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to save appointment. Please try again.');
|
||||
@@ -383,7 +415,7 @@ export const AppointmentForm = ({
|
||||
},
|
||||
);
|
||||
notify.success('Appointment Cancelled');
|
||||
onSaved?.();
|
||||
onSaved?.('CANCELLED');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to cancel appointment');
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user