feat: Phase 1 — agent status toggle, global search, enquiry form

- Agent status toggle: Ready/Break/Training/Offline with Ozonetel sync
- Global search: cross-entity search (leads + patients + appointments) via sidecar
- General enquiry form: capture caller questions during calls
- Button standard: icon-only for toggles, text+icon for primary actions
- Sidecar: agent-state endpoint, search module with platform queries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 14:21:40 +05:30
parent 721c2879ec
commit c3604377b9
6 changed files with 697 additions and 88 deletions

View File

@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faPhone, faPhoneHangup, faMicrophone, faMicrophoneSlash,
faPause, faPlay, faCalendarPlus, faCheckCircle,
faPhoneArrowRight, faRecordVinyl,
faPhoneArrowRight, faRecordVinyl, faClipboardQuestion,
} from '@fortawesome/pro-duotone-svg-icons';
import { Button } from '@/components/base/buttons/button';
import { Badge } from '@/components/base/badges/badges';
@@ -14,8 +14,10 @@ import { useSip } from '@/providers/sip-provider';
import { DispositionForm } from './disposition-form';
import { AppointmentForm } from './appointment-form';
import { TransferDialog } from './transfer-dialog';
import { EnquiryForm } from './enquiry-form';
import { formatPhone } from '@/lib/format';
import { apiClient } from '@/lib/api-client';
import { cx } from '@/utils/cx';
import { notify } from '@/lib/toast';
import type { Lead, CallDisposition } from '@/types/entities';
@@ -43,6 +45,7 @@ export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => {
const [appointmentBookedDuringCall, setAppointmentBookedDuringCall] = useState(false);
const [transferOpen, setTransferOpen] = useState(false);
const [recordingPaused, setRecordingPaused] = useState(false);
const [enquiryOpen, setEnquiryOpen] = useState(false);
// Capture direction at mount — survives through disposition stage
const callDirectionRef = useRef(callState === 'ringing-out' ? 'OUTBOUND' : 'INBOUND');
@@ -242,30 +245,57 @@ export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => {
</div>
<Badge size="md" color="success" type="pill-color">{formatDuration(callDuration)}</Badge>
</div>
<div className="mt-3 flex gap-2">
<Button size="sm" color={isMuted ? 'primary-destructive' : 'secondary'}
iconLeading={({ className }: { className?: string }) => <FontAwesomeIcon icon={isMuted ? faMicrophoneSlash : faMicrophone} className={className} />}
onClick={toggleMute}>{isMuted ? 'Unmute' : 'Mute'}</Button>
<Button size="sm" color={isOnHold ? 'primary-destructive' : 'secondary'}
iconLeading={({ className }: { className?: string }) => <FontAwesomeIcon icon={isOnHold ? faPlay : faPause} className={className} />}
onClick={toggleHold}>{isOnHold ? 'Resume' : 'Hold'}</Button>
<Button size="sm" color="secondary"
iconLeading={({ className }: { className?: string }) => <FontAwesomeIcon icon={faCalendarPlus} className={className} />}
onClick={() => setAppointmentOpen(true)}>Book Appt</Button>
<Button size="sm" color="secondary"
iconLeading={({ className }: { className?: string }) => <FontAwesomeIcon icon={faPhoneArrowRight} className={className} />}
onClick={() => setTransferOpen(!transferOpen)}>Transfer</Button>
<Button size="sm" color={recordingPaused ? 'primary-destructive' : 'secondary'}
iconLeading={({ className }: { className?: string }) => <FontAwesomeIcon icon={faRecordVinyl} className={className} />}
<div className="mt-3 flex items-center gap-1.5">
{/* Icon-only toggles */}
<button
onClick={toggleMute}
title={isMuted ? 'Unmute' : 'Mute'}
className={cx(
'flex size-8 items-center justify-center rounded-lg transition duration-100 ease-linear',
isMuted ? 'bg-error-solid text-white' : 'bg-secondary text-fg-quaternary hover:bg-secondary_hover hover:text-fg-secondary',
)}
>
<FontAwesomeIcon icon={isMuted ? faMicrophoneSlash : faMicrophone} className="size-3.5" />
</button>
<button
onClick={toggleHold}
title={isOnHold ? 'Resume' : 'Hold'}
className={cx(
'flex size-8 items-center justify-center rounded-lg transition duration-100 ease-linear',
isOnHold ? 'bg-warning-solid text-white' : 'bg-secondary text-fg-quaternary hover:bg-secondary_hover hover:text-fg-secondary',
)}
>
<FontAwesomeIcon icon={isOnHold ? faPlay : faPause} className="size-3.5" />
</button>
<button
onClick={() => {
const action = recordingPaused ? 'unPause' : 'pause';
if (callUcid) {
apiClient.post('/api/ozonetel/recording', { ucid: callUcid, action }).catch(() => {});
}
if (callUcid) apiClient.post('/api/ozonetel/recording', { ucid: callUcid, action }).catch(() => {});
setRecordingPaused(!recordingPaused);
}}>{recordingPaused ? 'Resume Rec' : 'Pause Rec'}</Button>
}}
title={recordingPaused ? 'Resume Recording' : 'Pause Recording'}
className={cx(
'flex size-8 items-center justify-center rounded-lg transition duration-100 ease-linear',
recordingPaused ? 'bg-error-solid text-white' : 'bg-secondary text-fg-quaternary hover:bg-secondary_hover hover:text-fg-secondary',
)}
>
<FontAwesomeIcon icon={faRecordVinyl} className="size-3.5" />
</button>
<div className="w-px h-6 bg-secondary mx-0.5" />
{/* Text+Icon primary actions */}
<Button size="sm" color="secondary"
iconLeading={({ className, ...rest }: any) => <FontAwesomeIcon icon={faCalendarPlus} className={className} {...rest} />}
onClick={() => setAppointmentOpen(true)}>Book Appt</Button>
<Button size="sm" color="secondary"
iconLeading={({ className, ...rest }: any) => <FontAwesomeIcon icon={faClipboardQuestion} className={className} {...rest} />}
onClick={() => setEnquiryOpen(!enquiryOpen)}>Enquiry</Button>
<Button size="sm" color="secondary"
iconLeading={({ className, ...rest }: any) => <FontAwesomeIcon icon={faPhoneArrowRight} className={className} {...rest} />}
onClick={() => setTransferOpen(!transferOpen)}>Transfer</Button>
<Button size="sm" color="primary-destructive" className="ml-auto"
iconLeading={({ className }: { className?: string }) => <FontAwesomeIcon icon={faPhoneHangup} className={className} />}
iconLeading={({ className, ...rest }: any) => <FontAwesomeIcon icon={faPhoneHangup} className={className} {...rest} />}
onClick={() => { hangup(); setPostCallStage('disposition'); }}>End</Button>
</div>
@@ -291,6 +321,17 @@ export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => {
leadId={lead?.id ?? null}
onSaved={handleAppointmentSaved}
/>
{/* Enquiry form */}
<EnquiryForm
isOpen={enquiryOpen}
onOpenChange={setEnquiryOpen}
callerPhone={callerPhone}
onSaved={() => {
setEnquiryOpen(false);
notify.success('Enquiry Logged');
}}
/>
</div>
);
}