mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
fix(appointment-form): filter past pills + confirm modal + view-only mode
- leadAppointments now filters out past appointments — past dates
can't be rescheduled, so "14 Apr · Meena Patel" shouldn't appear in
the pill row today. Uses scheduledAt >= now.
- Click Edit pill → reschedule-confirm modal:
"Yes, reschedule" → form opens in edit mode (prefilled + editable)
"No, just view" → form opens read-only (prefilled + disabled)
- Prefill was broken — AppointmentForm's useState initializers only
run at mount, so switching pills didn't re-seed state. Added
key={editingApptId}-{apptMode} so the form fully remounts whenever
the selection or mode changes.
- Thread readOnly prop through every form control (patient name,
phone, age, gender, clinic, department, doctor, date, time slots,
chief complaint). In view mode all inputs are disabled and the
Update Appointment + Cancel Appointment buttons hide — only Close
remains.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import { setOutboundPending } from '@/state/sip-manager';
|
||||
import { useSip } from '@/providers/sip-provider';
|
||||
import { DispositionModal } from './disposition-modal';
|
||||
import type { CallAction } from './disposition-modal';
|
||||
import { Dialog, Modal, ModalOverlay } from '@/components/application/modals/modal';
|
||||
import { AppointmentForm } from './appointment-form';
|
||||
import { TransferDialog } from './transfer-dialog';
|
||||
import { EnquiryForm } from './enquiry-form';
|
||||
@@ -74,12 +75,17 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
const leadAppointments = useMemo(() => {
|
||||
const patientId = (lead as any)?.patientId;
|
||||
if (!patientId) return [];
|
||||
const now = Date.now();
|
||||
return appointments
|
||||
.filter((a) =>
|
||||
a.patientId === patientId
|
||||
&& a.appointmentStatus !== 'CANCELLED'
|
||||
&& a.appointmentStatus !== 'NO_SHOW'
|
||||
&& a.appointmentStatus !== 'COMPLETED',
|
||||
&& a.appointmentStatus !== 'COMPLETED'
|
||||
// Only future appointments make sense as reschedule targets.
|
||||
// Past ones can't be edited — they already happened.
|
||||
&& a.scheduledAt
|
||||
&& new Date(a.scheduledAt).getTime() >= now,
|
||||
)
|
||||
.sort((a, b) => new Date(a.scheduledAt ?? '').getTime() - new Date(b.scheduledAt ?? '').getTime());
|
||||
}, [appointments, lead]);
|
||||
@@ -89,6 +95,12 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
[leadAppointments, editingApptId],
|
||||
);
|
||||
|
||||
// Pending pill click awaiting the reschedule-confirm modal. When the
|
||||
// agent clicks a pill, we store the appointment id here + open the modal.
|
||||
// Yes → promote to editingApptId in edit mode. No → promote in view mode.
|
||||
const [pendingApptId, setPendingApptId] = useState<string | null>(null);
|
||||
const [apptMode, setApptMode] = useState<'edit' | 'view'>('edit');
|
||||
|
||||
const agentConfig = localStorage.getItem('helix_agent_config');
|
||||
const agentIdForState = agentConfig ? (() => { try { return JSON.parse(agentConfig).ozonetelAgentId; } catch { return null; } })() : null;
|
||||
const { supervisorPresence } = useAgentState(agentIdForState);
|
||||
@@ -397,7 +409,7 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingApptId(appt.id)}
|
||||
onClick={() => setPendingApptId(appt.id)}
|
||||
className="flex items-center gap-1 rounded-md px-2 py-1 text-[11px] font-semibold text-brand-secondary hover:bg-brand-primary_hover transition duration-100 ease-linear"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPenToSquare} className="size-3" />
|
||||
@@ -408,16 +420,23 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Key forces a full remount when switching between
|
||||
pills (or between edit/view modes) so the form's
|
||||
internal state re-initializes from the new
|
||||
existingAppointment prop instead of staying
|
||||
stuck on the first-mounted values. */}
|
||||
<AppointmentForm
|
||||
key={`${editingApptId ?? 'new'}-${apptMode}`}
|
||||
isOpen={appointmentOpen}
|
||||
onOpenChange={(open) => {
|
||||
setAppointmentOpen(open);
|
||||
if (!open) setEditingApptId(null);
|
||||
if (!open) { setEditingApptId(null); setApptMode('edit'); }
|
||||
}}
|
||||
callerNumber={callerPhone}
|
||||
leadName={fullName || null}
|
||||
leadId={lead?.id ?? null}
|
||||
patientId={(lead as any)?.patientId ?? null}
|
||||
readOnly={apptMode === 'view'}
|
||||
existingAppointment={editingAppt ? {
|
||||
id: editingAppt.id,
|
||||
scheduledAt: editingAppt.scheduledAt ?? '',
|
||||
@@ -430,6 +449,7 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
} : null}
|
||||
onSaved={(outcome) => {
|
||||
setEditingApptId(null);
|
||||
setApptMode('edit');
|
||||
handleAppointmentSaved(outcome);
|
||||
}}
|
||||
/>
|
||||
@@ -451,6 +471,58 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reschedule confirm modal — fires when the agent clicks Edit
|
||||
on an upcoming-appointment pill. Yes → open the form in
|
||||
edit mode (fields editable, Save button). No → open in
|
||||
view-only mode (fields disabled, Close button). */}
|
||||
<ModalOverlay
|
||||
isOpen={pendingApptId !== null}
|
||||
onOpenChange={(open) => { if (!open) setPendingApptId(null); }}
|
||||
isDismissable
|
||||
>
|
||||
<Modal className="sm:max-w-md">
|
||||
<Dialog>
|
||||
{() => (
|
||||
<div className="flex flex-col gap-4 rounded-xl bg-primary p-6 shadow-xl ring-1 ring-secondary">
|
||||
<h2 className="text-lg font-semibold text-primary">Reschedule this appointment?</h2>
|
||||
<p className="text-sm text-tertiary">
|
||||
Choose "Yes, reschedule" to change the date, time, or doctor.
|
||||
Choose "No, just view" to see the details without changing anything.
|
||||
</p>
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
if (pendingApptId) {
|
||||
setEditingApptId(pendingApptId);
|
||||
setApptMode('view');
|
||||
setPendingApptId(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
No, just view
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (pendingApptId) {
|
||||
setEditingApptId(pendingApptId);
|
||||
setApptMode('edit');
|
||||
setPendingApptId(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Yes, reschedule
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
|
||||
{/* Disposition Modal — the ONLY path to end a call */}
|
||||
<DispositionModal
|
||||
isOpen={dispositionOpen}
|
||||
|
||||
@@ -35,6 +35,11 @@ type AppointmentFormProps = {
|
||||
// CANCELLED each map to distinct disposition outcomes).
|
||||
onSaved?: (outcome: 'BOOKED' | 'RESCHEDULED' | 'CANCELLED') => void;
|
||||
existingAppointment?: ExistingAppointment | null;
|
||||
// When true, the form shows the existing appointment's data in a
|
||||
// disabled state — no input editing, no Save/Cancel. Only a Close
|
||||
// button. Used by the reschedule-confirm flow when the agent picks
|
||||
// "No, just view" on an upcoming-appointment pill.
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
type DoctorRecord = { id: string; name: string; department: string; clinic: string };
|
||||
@@ -60,6 +65,7 @@ export const AppointmentForm = ({
|
||||
patientId,
|
||||
onSaved,
|
||||
existingAppointment,
|
||||
readOnly = false,
|
||||
}: AppointmentFormProps) => {
|
||||
const isEditMode = !!existingAppointment;
|
||||
|
||||
@@ -471,7 +477,7 @@ export const AppointmentForm = ({
|
||||
placeholder="Full name"
|
||||
value={patientName}
|
||||
onChange={setPatientName}
|
||||
isDisabled={!isNameEditable}
|
||||
isDisabled={readOnly || !isNameEditable}
|
||||
/>
|
||||
</div>
|
||||
{!isNameEditable && initialLeadName.length > 0 && (
|
||||
@@ -544,7 +550,7 @@ export const AppointmentForm = ({
|
||||
items={departmentItems}
|
||||
selectedKey={department}
|
||||
onSelectionChange={(key) => setDepartment(key as string)}
|
||||
isDisabled={doctors.length === 0}
|
||||
isDisabled={readOnly || doctors.length === 0}
|
||||
>
|
||||
{(item) => <Select.Item id={item.id} label={item.label} />}
|
||||
</Select>
|
||||
@@ -555,7 +561,7 @@ export const AppointmentForm = ({
|
||||
items={doctorSelectItems}
|
||||
selectedKey={doctor}
|
||||
onSelectionChange={(key) => setDoctor(key as string)}
|
||||
isDisabled={!department}
|
||||
isDisabled={readOnly || !department}
|
||||
>
|
||||
{(item) => <Select.Item id={item.id} label={item.label} />}
|
||||
</Select>
|
||||
@@ -567,7 +573,7 @@ export const AppointmentForm = ({
|
||||
value={date ? parseDate(date) : null}
|
||||
onChange={(val) => setDate(val ? val.toString() : '')}
|
||||
granularity="day"
|
||||
isDisabled={!doctor}
|
||||
isDisabled={readOnly || !doctor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -585,7 +591,7 @@ export const AppointmentForm = ({
|
||||
<button
|
||||
key={slot.id}
|
||||
type="button"
|
||||
disabled={isBooked}
|
||||
disabled={readOnly || isBooked}
|
||||
onClick={() => setTimeSlot(slot.id)}
|
||||
className={cx(
|
||||
'rounded-lg py-2 px-1 text-xs font-medium transition duration-100 ease-linear',
|
||||
@@ -613,6 +619,7 @@ export const AppointmentForm = ({
|
||||
placeholder="Describe the reason for visit..."
|
||||
value={chiefComplaint}
|
||||
onChange={setChiefComplaint}
|
||||
isDisabled={readOnly}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
@@ -649,7 +656,7 @@ export const AppointmentForm = ({
|
||||
{/* Footer — pinned */}
|
||||
<div className="shrink-0 flex items-center justify-between pt-4 border-t border-secondary">
|
||||
<div>
|
||||
{isEditMode && (
|
||||
{isEditMode && !readOnly && (
|
||||
<Button size="sm" color="primary-destructive" isLoading={isSaving} onClick={handleCancel}>
|
||||
Cancel Appointment
|
||||
</Button>
|
||||
@@ -659,9 +666,11 @@ export const AppointmentForm = ({
|
||||
<Button size="sm" color="secondary" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
{!readOnly && (
|
||||
<Button size="sm" color="primary" isLoading={isSaving} showTextWhileLoading onClick={handleSave}>
|
||||
{isSaving ? 'Saving...' : isEditMode ? 'Update Appointment' : 'Book Appointment'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user