mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
- Worklist default sort descending (newest first), sortable column headers (PRIORITY, PATIENT, SLA) via React Aria - Contextual disposition: auto-selects based on in-call actions (appointment → APPOINTMENT_BOOKED, enquiry → INFO_PROVIDED, transfer → FOLLOW_UP_SCHEDULED) - Context panel redesign: collapsible AI Insight, Upcoming (appointments + follow-ups + linked patient), Recent (calls + activities) sections; auto-collapse on AI chat start - Appointments added to DataProvider with APPOINTMENTS_QUERY, Appointment type, transform - Notification bell for admin/supervisor: performance alerts (idle time, NPS, conversion thresholds) with toast on load + bell dropdown with dismiss; demo alerts as fallback - Slideout z-index fix: added z-50 to slideout ModalOverlay matching modal component Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
170 lines
8.0 KiB
TypeScript
170 lines
8.0 KiB
TypeScript
import { useState, useRef } from 'react';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import { faPhoneHangup } from '@fortawesome/pro-duotone-svg-icons';
|
|
import { Dialog, Modal, ModalOverlay } from '@/components/application/modals/modal';
|
|
import { FeaturedIcon } from '@/components/foundations/featured-icon/featured-icon';
|
|
import { TextArea } from '@/components/base/textarea/textarea';
|
|
import type { FC } from 'react';
|
|
import type { CallDisposition } from '@/types/entities';
|
|
import { cx } from '@/utils/cx';
|
|
|
|
const PhoneHangUpIcon: FC<{ className?: string }> = ({ className }) => (
|
|
<FontAwesomeIcon icon={faPhoneHangup} className={className} />
|
|
);
|
|
|
|
const dispositionOptions: Array<{
|
|
value: CallDisposition;
|
|
label: string;
|
|
activeClass: string;
|
|
defaultClass: string;
|
|
}> = [
|
|
{
|
|
value: 'APPOINTMENT_BOOKED',
|
|
label: 'Appointment Booked',
|
|
activeClass: 'bg-success-solid text-white border-transparent',
|
|
defaultClass: 'bg-success-primary text-success-primary border-success',
|
|
},
|
|
{
|
|
value: 'FOLLOW_UP_SCHEDULED',
|
|
label: 'Follow-up Needed',
|
|
activeClass: 'bg-brand-solid text-white border-transparent',
|
|
defaultClass: 'bg-brand-primary text-brand-secondary border-brand',
|
|
},
|
|
{
|
|
value: 'INFO_PROVIDED',
|
|
label: 'Info Provided',
|
|
activeClass: 'bg-utility-blue-light-600 text-white border-transparent',
|
|
defaultClass: 'bg-utility-blue-light-50 text-utility-blue-light-700 border-utility-blue-light-200',
|
|
},
|
|
{
|
|
value: 'NO_ANSWER',
|
|
label: 'No Answer',
|
|
activeClass: 'bg-warning-solid text-white border-transparent',
|
|
defaultClass: 'bg-warning-primary text-warning-primary border-warning',
|
|
},
|
|
{
|
|
value: 'WRONG_NUMBER',
|
|
label: 'Wrong Number',
|
|
activeClass: 'bg-secondary-solid text-white border-transparent',
|
|
defaultClass: 'bg-secondary text-secondary border-secondary',
|
|
},
|
|
{
|
|
value: 'CALLBACK_REQUESTED',
|
|
label: 'Not Interested',
|
|
activeClass: 'bg-error-solid text-white border-transparent',
|
|
defaultClass: 'bg-error-primary text-error-primary border-error',
|
|
},
|
|
];
|
|
|
|
type DispositionModalProps = {
|
|
isOpen: boolean;
|
|
callerName: string;
|
|
callerDisconnected: boolean;
|
|
defaultDisposition?: CallDisposition | null;
|
|
onSubmit: (disposition: CallDisposition, notes: string) => void;
|
|
onDismiss?: () => void;
|
|
};
|
|
|
|
export const DispositionModal = ({ isOpen, callerName, callerDisconnected, defaultDisposition, onSubmit, onDismiss }: DispositionModalProps) => {
|
|
const [selected, setSelected] = useState<CallDisposition | null>(null);
|
|
const [notes, setNotes] = useState('');
|
|
const appliedDefaultRef = useRef<CallDisposition | null | undefined>(undefined);
|
|
|
|
// Pre-select when modal opens with a suggestion
|
|
if (isOpen && defaultDisposition && appliedDefaultRef.current !== defaultDisposition) {
|
|
appliedDefaultRef.current = defaultDisposition;
|
|
setSelected(defaultDisposition);
|
|
}
|
|
|
|
const handleSubmit = () => {
|
|
if (selected === null) return;
|
|
onSubmit(selected, notes);
|
|
setSelected(null);
|
|
setNotes('');
|
|
appliedDefaultRef.current = undefined;
|
|
};
|
|
|
|
return (
|
|
<ModalOverlay isOpen={isOpen} isDismissable onOpenChange={(open) => { if (!open && onDismiss) onDismiss(); }}>
|
|
<Modal className="sm:max-w-md">
|
|
<Dialog>
|
|
{() => (
|
|
<div className="flex w-full flex-col rounded-xl bg-primary shadow-xl ring-1 ring-secondary overflow-hidden">
|
|
{/* Header */}
|
|
<div className="flex flex-col items-center gap-3 px-6 pt-6 pb-4">
|
|
<FeaturedIcon icon={PhoneHangUpIcon} color={callerDisconnected ? 'warning' : 'error'} theme="light" size="md" />
|
|
<div className="text-center">
|
|
<h2 className="text-lg font-semibold text-primary">
|
|
{callerDisconnected ? 'Call Disconnected' : 'End Call'}
|
|
</h2>
|
|
<p className="mt-1 text-sm text-tertiary">
|
|
{callerDisconnected
|
|
? `${callerName} disconnected. What was the outcome?`
|
|
: `Select a reason to end the call with ${callerName}.`
|
|
}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Disposition options */}
|
|
<div className="px-6 pb-4">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{dispositionOptions.map((option) => {
|
|
const isSelected = selected === option.value;
|
|
return (
|
|
<button
|
|
key={option.value}
|
|
type="button"
|
|
onClick={() => setSelected(option.value)}
|
|
className={cx(
|
|
'cursor-pointer rounded-xl border-2 p-3 text-sm font-semibold transition duration-100 ease-linear',
|
|
isSelected
|
|
? cx(option.activeClass, 'ring-2 ring-brand')
|
|
: option.defaultClass,
|
|
)}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="mt-3">
|
|
<TextArea
|
|
label="Notes (optional)"
|
|
placeholder="Add any notes about this call..."
|
|
value={notes}
|
|
onChange={(value) => setNotes(value)}
|
|
rows={2}
|
|
textAreaClassName="text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="border-t border-secondary px-6 py-4">
|
|
<button
|
|
type="button"
|
|
onClick={handleSubmit}
|
|
disabled={selected === null}
|
|
className={cx(
|
|
'w-full rounded-xl px-6 py-2.5 text-sm font-semibold transition duration-100 ease-linear',
|
|
selected !== null
|
|
? 'cursor-pointer bg-error-solid text-white hover:bg-error-solid_hover'
|
|
: 'cursor-not-allowed bg-disabled text-disabled',
|
|
)}
|
|
>
|
|
{callerDisconnected
|
|
? (selected ? 'Submit & Close' : 'Select a reason')
|
|
: (selected ? 'End Call & Submit' : 'Select a reason to end call')
|
|
}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Dialog>
|
|
</Modal>
|
|
</ModalOverlay>
|
|
);
|
|
};
|