mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
#533: Remove redundant Call History top header (duplicate TopBar) #531: Block logout during active call (confirm dialog + UCID check) #529: Block outbound calls when agent is on Break/Training #527: Remove updatePatient during appointment creation (was mutating shared Patient entity, affecting all past appointments) #547: SLA rules seeded via API (config issue, not code) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
606 lines
27 KiB
TypeScript
606 lines
27 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import { faUserPen } from '@fortawesome/pro-duotone-svg-icons';
|
|
import { Input } from '@/components/base/input/input';
|
|
import { Select } from '@/components/base/select/select';
|
|
import { TextArea } from '@/components/base/textarea/textarea';
|
|
import { Button } from '@/components/base/buttons/button';
|
|
import { DatePicker } from '@/components/application/date-picker/date-picker';
|
|
import { parseDate } from '@internationalized/date';
|
|
import { apiClient } from '@/lib/api-client';
|
|
import { cx } from '@/utils/cx';
|
|
import { notify } from '@/lib/toast';
|
|
import { EditPatientConfirmModal } from '@/components/modals/edit-patient-confirm-modal';
|
|
|
|
type ExistingAppointment = {
|
|
id: string;
|
|
scheduledAt: string;
|
|
doctorName: string;
|
|
doctorId?: string;
|
|
department: string;
|
|
reasonForVisit?: string;
|
|
status: string;
|
|
};
|
|
|
|
type AppointmentFormProps = {
|
|
isOpen: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
callerNumber?: string | null;
|
|
leadName?: string | null;
|
|
leadId?: string | null;
|
|
patientId?: string | null;
|
|
onSaved?: () => void;
|
|
existingAppointment?: ExistingAppointment | null;
|
|
};
|
|
|
|
type DoctorRecord = { id: string; name: string; department: string; clinic: string };
|
|
|
|
// Clinics are fetched dynamically from the platform — no hardcoded list.
|
|
// If the workspace has no clinics configured, the dropdown shows empty.
|
|
|
|
const genderItems = [
|
|
{ id: 'male', label: 'Male' },
|
|
{ id: 'female', label: 'Female' },
|
|
{ id: 'other', label: 'Other' },
|
|
];
|
|
|
|
// Time slots are fetched from /api/masterdata/slots based on
|
|
// doctor + date. No hardcoded times.
|
|
|
|
export const AppointmentForm = ({
|
|
isOpen,
|
|
onOpenChange,
|
|
callerNumber,
|
|
leadName,
|
|
leadId,
|
|
patientId,
|
|
onSaved,
|
|
existingAppointment,
|
|
}: AppointmentFormProps) => {
|
|
const isEditMode = !!existingAppointment;
|
|
|
|
// Doctor data from platform
|
|
const [doctors, setDoctors] = useState<DoctorRecord[]>([]);
|
|
|
|
// Initial name captured at form open — used to detect whether the
|
|
// agent actually changed the name before we commit any destructive
|
|
// updatePatient / updateLead.contactName mutations.
|
|
const initialLeadName = (leadName ?? '').trim();
|
|
|
|
// Form state — initialized from existing appointment in edit mode
|
|
const [patientName, setPatientName] = useState(leadName ?? '');
|
|
// The patient-name input is locked by default when there's an
|
|
// existing caller name (to prevent accidental rename-on-save), and
|
|
// unlocked only after the agent clicks the Edit button and confirms
|
|
// in the warning modal. First-time callers with no existing name
|
|
// start unlocked because there's nothing to protect.
|
|
const [isNameEditable, setIsNameEditable] = useState(initialLeadName.length === 0);
|
|
const [editConfirmOpen, setEditConfirmOpen] = useState(false);
|
|
const [patientPhone, setPatientPhone] = useState(callerNumber ?? '');
|
|
const [age, setAge] = useState('');
|
|
const [gender, setGender] = useState<string | null>(null);
|
|
const [clinic, setClinic] = useState<string | null>(null);
|
|
const [clinicItems, setClinicItems] = useState<Array<{ id: string; label: string }>>([]);
|
|
const [department, setDepartment] = useState<string | null>(existingAppointment?.department ?? null);
|
|
const [doctor, setDoctor] = useState<string | null>(existingAppointment?.doctorId ?? null);
|
|
const [date, setDate] = useState(() => {
|
|
if (existingAppointment?.scheduledAt) return existingAppointment.scheduledAt.split('T')[0];
|
|
return new Date().toISOString().split('T')[0];
|
|
});
|
|
const [timeSlot, setTimeSlot] = useState<string | null>(() => {
|
|
if (existingAppointment?.scheduledAt) {
|
|
const dt = new Date(existingAppointment.scheduledAt);
|
|
return `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}`;
|
|
}
|
|
return null;
|
|
});
|
|
const [chiefComplaint, setChiefComplaint] = useState(existingAppointment?.reasonForVisit ?? '');
|
|
const [source, setSource] = useState('Inbound Call');
|
|
const [agentNotes, setAgentNotes] = useState('');
|
|
const [timeSlotItems, setTimeSlotItems] = useState<Array<{ id: string; label: string }>>([]);
|
|
|
|
// Fetch available time slots when doctor + date change
|
|
useEffect(() => {
|
|
if (!doctor || !date) {
|
|
setTimeSlotItems([]);
|
|
return;
|
|
}
|
|
apiClient.get<Array<{ time: string; label: string; clinicId: string; clinicName: string }>>(
|
|
`/api/masterdata/slots?doctorId=${doctor}&date=${date}`,
|
|
).then(slots => {
|
|
setTimeSlotItems(slots.map(s => ({ id: s.time, label: s.label })));
|
|
// Auto-select clinic from the slot's clinic
|
|
if (slots.length > 0 && !clinic) {
|
|
setClinic(slots[0].clinicId);
|
|
}
|
|
}).catch(() => setTimeSlotItems([]));
|
|
}, [doctor, date]);
|
|
|
|
// Availability state
|
|
const [bookedSlots, setBookedSlots] = useState<string[]>([]);
|
|
const [loadingSlots, setLoadingSlots] = useState(false);
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Fetch doctors on mount. Doctors are hospital-wide — no single
|
|
// `clinic` field anymore. We pull the full visit-slot list via the
|
|
// Fetch clinics + doctors from the master data endpoint (Redis-cached).
|
|
// This is faster than direct GraphQL and returns pre-formatted data.
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
apiClient.get<Array<{ id: string; name: string; phone: string; address: string }>>('/api/masterdata/clinics')
|
|
.then(clinics => {
|
|
setClinicItems(clinics.map(c => ({ id: c.id, label: c.name || 'Unnamed Clinic' })));
|
|
}).catch(() => {});
|
|
}, [isOpen]);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
apiClient.get<Array<{ id: string; name: string; department: string; qualifications: string }>>('/api/masterdata/doctors')
|
|
.then(docs => {
|
|
setDoctors(docs.map(d => ({
|
|
id: d.id,
|
|
name: d.name,
|
|
department: d.department,
|
|
clinic: '', // clinic assignment via visit slots, not on doctor directly
|
|
})));
|
|
}).catch(() => {});
|
|
}, [isOpen]);
|
|
|
|
// Fetch booked slots when doctor + date selected
|
|
useEffect(() => {
|
|
if (!doctor || !date) {
|
|
setBookedSlots([]);
|
|
return;
|
|
}
|
|
|
|
setLoadingSlots(true);
|
|
apiClient.graphql<{ appointments: { edges: Array<{ node: any }> } }>(
|
|
`{ appointments(filter: {
|
|
doctorId: { eq: "${doctor}" },
|
|
scheduledAt: { gte: "${date}T00:00:00", lte: "${date}T23:59:59" }
|
|
}) { edges { node { id scheduledAt durationMin status } } } }`,
|
|
).then(data => {
|
|
// Filter out cancelled/completed appointments client-side
|
|
const activeAppointments = data.appointments.edges.filter(e => {
|
|
const status = e.node.status;
|
|
return status !== 'CANCELLED' && status !== 'COMPLETED' && status !== 'NO_SHOW';
|
|
});
|
|
const slots = activeAppointments.map(e => {
|
|
const dt = new Date(e.node.scheduledAt);
|
|
return `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}`;
|
|
});
|
|
// In edit mode, don't block the current appointment's slot
|
|
if (isEditMode && existingAppointment) {
|
|
const currentDt = new Date(existingAppointment.scheduledAt);
|
|
const currentSlot = `${currentDt.getHours().toString().padStart(2, '0')}:${currentDt.getMinutes().toString().padStart(2, '0')}`;
|
|
setBookedSlots(slots.filter(s => s !== currentSlot));
|
|
} else {
|
|
setBookedSlots(slots);
|
|
}
|
|
}).catch(() => setBookedSlots([]))
|
|
.finally(() => setLoadingSlots(false));
|
|
}, [doctor, date, isEditMode, existingAppointment]);
|
|
|
|
// Reset doctor when department changes
|
|
useEffect(() => {
|
|
setDoctor(null);
|
|
setTimeSlot(null);
|
|
}, [department]);
|
|
|
|
// Reset time slot when doctor or date changes
|
|
useEffect(() => {
|
|
setTimeSlot(null);
|
|
}, [doctor, date]);
|
|
|
|
// Departments from master data (or fallback to deriving from doctors)
|
|
const [departmentItems, setDepartmentItems] = useState<Array<{ id: string; label: string }>>([]);
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
apiClient.get<string[]>('/api/masterdata/departments')
|
|
.then(depts => setDepartmentItems(depts.map(d => ({ id: d, label: d }))))
|
|
.catch(() => {
|
|
// Fallback: derive from doctor list
|
|
const derived = [...new Set(doctors.map(d => d.department).filter(Boolean))];
|
|
setDepartmentItems(derived.map(d => ({ id: d, label: d })));
|
|
});
|
|
}, [isOpen, doctors]);
|
|
|
|
const filteredDoctors = department
|
|
? doctors.filter(d => d.department === department)
|
|
: doctors;
|
|
const doctorSelectItems = filteredDoctors.map(d => ({ id: d.id, label: d.name }));
|
|
|
|
const timeSlotSelectItems = timeSlotItems.map(slot => ({
|
|
...slot,
|
|
isDisabled: bookedSlots.includes(slot.id),
|
|
label: bookedSlots.includes(slot.id) ? `${slot.label} (Booked)` : slot.label,
|
|
}));
|
|
|
|
const handleSave = async () => {
|
|
if (!date || !timeSlot || !doctor || !department) {
|
|
setError('Please fill in the required fields: date, time, doctor, and department.');
|
|
return;
|
|
}
|
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
if (!isEditMode && date < today) {
|
|
setError('Appointment date cannot be in the past.');
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const scheduledAt = new Date(`${date}T${timeSlot}:00`).toISOString();
|
|
const selectedDoctor = doctors.find(d => d.id === doctor);
|
|
|
|
if (isEditMode && existingAppointment) {
|
|
// Update existing appointment
|
|
await apiClient.graphql(
|
|
`mutation UpdateAppointment($id: UUID!, $data: AppointmentUpdateInput!) {
|
|
updateAppointment(id: $id, data: $data) { id }
|
|
}`,
|
|
{
|
|
id: existingAppointment.id,
|
|
data: {
|
|
scheduledAt,
|
|
doctorName: selectedDoctor?.name ?? '',
|
|
department: selectedDoctor?.department ?? '',
|
|
doctorId: doctor,
|
|
reasonForVisit: chiefComplaint || null,
|
|
},
|
|
},
|
|
);
|
|
notify.success('Appointment Updated');
|
|
} else {
|
|
// Create appointment
|
|
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,
|
|
...(patientId ? { patientId } : {}),
|
|
},
|
|
},
|
|
);
|
|
|
|
// Determine whether the agent actually renamed the patient.
|
|
// Only a non-empty, changed-from-initial name counts — empty
|
|
// strings or an unchanged name never trigger the rename
|
|
// chain, even if the field was unlocked.
|
|
const trimmedName = patientName.trim();
|
|
const nameChanged = isNameEditable && trimmedName.length > 0 && trimmedName !== initialLeadName;
|
|
|
|
// DO NOT update the shared Patient entity when name changes
|
|
// during appointment creation. The Patient record is shared
|
|
// across all appointments — modifying it here would
|
|
// retroactively change the name on all past appointments.
|
|
// The patient name for THIS appointment is stored on the
|
|
// Appointment entity itself (via doctorName/department).
|
|
// Bug #527: removed updatePatient() call.
|
|
|
|
// Update lead status/lastContacted on every appointment book
|
|
// (those are genuinely about this appointment), but only
|
|
// touch lead.contactName if the agent explicitly renamed.
|
|
//
|
|
// NOTE: field name is `status`, NOT `leadStatus` — the
|
|
// staging platform schema renamed this. The old name is
|
|
// rejected by LeadUpdateInput.
|
|
if (leadId) {
|
|
await apiClient.graphql(
|
|
`mutation UpdateLead($id: UUID!, $data: LeadUpdateInput!) {
|
|
updateLead(id: $id, data: $data) { id }
|
|
}`,
|
|
{
|
|
id: leadId,
|
|
data: {
|
|
status: 'APPOINTMENT_SET',
|
|
lastContacted: new Date().toISOString(),
|
|
...(nameChanged ? { contactName: { firstName: trimmedName.split(' ')[0], lastName: trimmedName.split(' ').slice(1).join(' ') || '' } } : {}),
|
|
},
|
|
},
|
|
).catch((err: unknown) => console.warn('Failed to update lead:', err));
|
|
}
|
|
|
|
// 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.
|
|
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?.();
|
|
} catch (err) {
|
|
console.error('Failed to save appointment:', err);
|
|
setError(err instanceof Error ? err.message : 'Failed to save appointment. Please try again.');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = async () => {
|
|
if (!existingAppointment) return;
|
|
setIsSaving(true);
|
|
try {
|
|
await apiClient.graphql(
|
|
`mutation CancelAppointment($id: UUID!, $data: AppointmentUpdateInput!) {
|
|
updateAppointment(id: $id, data: $data) { id }
|
|
}`,
|
|
{
|
|
id: existingAppointment.id,
|
|
data: { status: 'CANCELLED' },
|
|
},
|
|
);
|
|
notify.success('Appointment Cancelled');
|
|
onSaved?.();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to cancel appointment');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="flex flex-col flex-1 min-h-0">
|
|
{/* Form fields — scrollable */}
|
|
<div className="flex-1 overflow-y-auto">
|
|
<div className="flex flex-col gap-3">
|
|
{/* Patient Info — only for new appointments */}
|
|
{!isEditMode && (
|
|
<>
|
|
<div className="flex flex-col gap-1">
|
|
<span className="text-xs font-bold uppercase tracking-wider text-tertiary">
|
|
Patient Information
|
|
</span>
|
|
</div>
|
|
|
|
{/* Patient name — locked by default for existing
|
|
callers, unlocked for new callers with no
|
|
prior name on record. The Edit button opens
|
|
a confirm modal before unlocking; see
|
|
EditPatientNameModal for the rationale. */}
|
|
<div className="flex items-end gap-2">
|
|
<div className="flex-1">
|
|
<Input
|
|
label="Patient Name"
|
|
placeholder="Full name"
|
|
value={patientName}
|
|
onChange={setPatientName}
|
|
isDisabled={!isNameEditable}
|
|
/>
|
|
</div>
|
|
{!isNameEditable && initialLeadName.length > 0 && (
|
|
<Button
|
|
size="sm"
|
|
color="secondary"
|
|
iconLeading={({ className }: { className?: string }) => (
|
|
<FontAwesomeIcon icon={faUserPen} className={className} />
|
|
)}
|
|
onClick={() => setEditConfirmOpen(true)}
|
|
>
|
|
Edit
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<Input
|
|
label="Phone"
|
|
placeholder="Phone number"
|
|
value={patientPhone}
|
|
onChange={setPatientPhone}
|
|
/>
|
|
<Input
|
|
label="Age"
|
|
placeholder="Age"
|
|
type="number"
|
|
value={age}
|
|
onChange={setAge}
|
|
/>
|
|
</div>
|
|
|
|
<Select
|
|
label="Gender"
|
|
placeholder="Select gender"
|
|
items={genderItems}
|
|
selectedKey={gender}
|
|
onSelectionChange={(key) => setGender(key as string)}
|
|
>
|
|
{(item) => <Select.Item id={item.id} label={item.label} />}
|
|
</Select>
|
|
|
|
<div className="border-t border-secondary" />
|
|
</>
|
|
)}
|
|
|
|
{/* Appointment Details */}
|
|
<div className="flex flex-col gap-1">
|
|
<span className="text-xs font-bold uppercase tracking-wider text-tertiary">
|
|
Appointment Details
|
|
</span>
|
|
</div>
|
|
|
|
{!isEditMode && (
|
|
<Select
|
|
label="Clinic / Branch"
|
|
placeholder="Select clinic"
|
|
items={clinicItems}
|
|
selectedKey={clinic}
|
|
onSelectionChange={(key) => setClinic(key as string)}
|
|
>
|
|
{(item) => <Select.Item id={item.id} label={item.label} />}
|
|
</Select>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<Select
|
|
label="Department *"
|
|
placeholder={doctors.length === 0 ? 'Loading...' : 'Select department'}
|
|
items={departmentItems}
|
|
selectedKey={department}
|
|
onSelectionChange={(key) => setDepartment(key as string)}
|
|
isDisabled={doctors.length === 0}
|
|
>
|
|
{(item) => <Select.Item id={item.id} label={item.label} />}
|
|
</Select>
|
|
|
|
<Select
|
|
label="Doctor *"
|
|
placeholder={!department ? 'Select department first' : 'Select doctor'}
|
|
items={doctorSelectItems}
|
|
selectedKey={doctor}
|
|
onSelectionChange={(key) => setDoctor(key as string)}
|
|
isDisabled={!department}
|
|
>
|
|
{(item) => <Select.Item id={item.id} label={item.label} />}
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1">
|
|
<span className="text-xs font-medium text-secondary">Date <span className="text-error-primary">*</span></span>
|
|
<DatePicker
|
|
value={date ? parseDate(date) : null}
|
|
onChange={(val) => setDate(val ? val.toString() : '')}
|
|
granularity="day"
|
|
isDisabled={!doctor}
|
|
/>
|
|
</div>
|
|
|
|
{/* Time slot grid */}
|
|
{doctor && date && (
|
|
<div className="flex flex-col gap-2">
|
|
<span className="text-xs font-semibold text-secondary">
|
|
{loadingSlots ? 'Checking availability...' : 'Available Slots'}
|
|
</span>
|
|
<div className="grid grid-cols-4 gap-1.5">
|
|
{timeSlotSelectItems.map(slot => {
|
|
const isBooked = slot.isDisabled;
|
|
const isSelected = timeSlot === slot.id;
|
|
return (
|
|
<button
|
|
key={slot.id}
|
|
type="button"
|
|
disabled={isBooked}
|
|
onClick={() => setTimeSlot(slot.id)}
|
|
className={cx(
|
|
'rounded-lg py-2 px-1 text-xs font-medium transition duration-100 ease-linear',
|
|
isBooked
|
|
? 'cursor-not-allowed bg-disabled text-disabled line-through'
|
|
: isSelected
|
|
? 'bg-brand-solid text-white ring-2 ring-brand'
|
|
: 'cursor-pointer bg-secondary text-secondary hover:bg-secondary_hover hover:text-secondary_hover',
|
|
)}
|
|
>
|
|
{timeSlotItems.find(t => t.id === slot.id)?.label ?? slot.id}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{!doctor || !date ? (
|
|
<p className="text-xs text-tertiary">Select a doctor and date to see available time slots</p>
|
|
) : null}
|
|
|
|
<TextArea
|
|
label="Chief Complaint"
|
|
placeholder="Describe the reason for visit..."
|
|
value={chiefComplaint}
|
|
onChange={setChiefComplaint}
|
|
rows={2}
|
|
/>
|
|
|
|
{/* Additional Info — only for new appointments */}
|
|
{!isEditMode && (
|
|
<>
|
|
<div className="border-t border-secondary" />
|
|
|
|
<Input
|
|
label="Source / Referral"
|
|
placeholder="How did the patient reach us?"
|
|
value={source}
|
|
onChange={setSource}
|
|
/>
|
|
|
|
<TextArea
|
|
label="Agent Notes"
|
|
placeholder="Any additional notes..."
|
|
value={agentNotes}
|
|
onChange={setAgentNotes}
|
|
rows={2}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="rounded-lg bg-error-primary p-3 text-sm text-error-primary">
|
|
{error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer — pinned */}
|
|
<div className="shrink-0 flex items-center justify-between pt-4 border-t border-secondary">
|
|
<div>
|
|
{isEditMode && (
|
|
<Button size="sm" color="primary-destructive" isLoading={isSaving} onClick={handleCancel}>
|
|
Cancel Appointment
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Button size="sm" color="secondary" onClick={() => onOpenChange(false)}>
|
|
Close
|
|
</Button>
|
|
<Button size="sm" color="primary" isLoading={isSaving} showTextWhileLoading onClick={handleSave}>
|
|
{isSaving ? 'Saving...' : isEditMode ? 'Update Appointment' : 'Book Appointment'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<EditPatientConfirmModal
|
|
isOpen={editConfirmOpen}
|
|
onOpenChange={setEditConfirmOpen}
|
|
onConfirm={() => {
|
|
setIsNameEditable(true);
|
|
setEditConfirmOpen(false);
|
|
}}
|
|
description={
|
|
<>
|
|
You're about to change the name on this patient's record. This will
|
|
update their profile across Helix Engage, including past appointments,
|
|
lead history, and AI summary. Only proceed if the current name is
|
|
actually wrong — for all other cases, cancel and continue with the
|
|
appointment as-is.
|
|
</>
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|