feat: disposition modal, persistent top bar, pagination, QA fixes

- DispositionModal: single modal for all call endings. Dismissable (agent can resume call).
  Agent clicks End → modal → select reason → hangup + dispose. Caller disconnects → same modal.
- One call screen: CallWidget stripped to ringing notification + auto-redirect to Call Desk.
- Persistent top bar in AppShell: agent status toggle + network indicator on all pages.
- Network indicator always visible (Connected/Unstable/No connection).
- Pagination: Untitled UI PaginationCardDefault on Call History + Appointments (20/page).
- Pinned table headers/footers: sticky column headers, scrollable body, pinned pagination.
  Applied to Call Desk worklist, Call History, Appointments, Call Recordings, Missed Calls.
- "Patient" → "Caller" column label in Call History.
- Offline → Ready toggle enabled.
- Profile status dot reflects Ozonetel state.
- NavAccountCard: popover placement top, View Profile + Account Settings restored.
- WIP pages for /profile and /account-settings.
- Enquiry form PHONE_INQUIRY → PHONE enum fix.
- Force Ready / View Profile / Account Settings removed then restored properly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 20:29:54 +05:30
parent daa2fbb0c2
commit e6b2208077
21 changed files with 645 additions and 816 deletions

View File

@@ -0,0 +1,160 @@
import { useState } 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;
onSubmit: (disposition: CallDisposition, notes: string) => void;
onDismiss?: () => void;
};
export const DispositionModal = ({ isOpen, callerName, callerDisconnected, onSubmit, onDismiss }: DispositionModalProps) => {
const [selected, setSelected] = useState<CallDisposition | null>(null);
const [notes, setNotes] = useState('');
const handleSubmit = () => {
if (selected === null) return;
onSubmit(selected, notes);
setSelected(null);
setNotes('');
};
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>
);
};