mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
feat: CC agent features, live call assist, worklist redesign, brand tokens
CC Agent: - Call transfer (CONFERENCE + KICK_CALL) with inline transfer dialog - Recording pause/resume during active calls - Missed calls API (Ozonetel abandonCalls) - Call history API (Ozonetel fetchCDRDetails) Live Call Assist: - Deepgram Nova STT via raw WebSocket - OpenAI suggestions every 10s with lead context - LiveTranscript component in sidebar during calls - Browser audio capture from remote WebRTC stream Worklist: - Redesigned table: clickable phones, context menu (Call/SMS/WhatsApp) - Last interaction sub-line, source column, improved SLA - Filtered out rows without phone numbers - New missed call notifications Brand: - Logo on login page - Blue scale rebuilt from logo blue rgb(32, 96, 160) - FontAwesome duotone CSS variables set globally - Profile menu icons switched to duotone Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
150
src/components/call-desk/phone-action-cell.tsx
Normal file
150
src/components/call-desk/phone-action-cell.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPhone, faCommentDots, faEllipsisVertical, faMessageDots } from '@fortawesome/pro-duotone-svg-icons';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useSip } from '@/providers/sip-provider';
|
||||
import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom } from '@/state/sip-state';
|
||||
import { setOutboundPending } from '@/state/sip-manager';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { notify } from '@/lib/toast';
|
||||
import { cx } from '@/utils/cx';
|
||||
|
||||
type PhoneActionCellProps = {
|
||||
phoneNumber: string;
|
||||
displayNumber: string;
|
||||
leadId?: string;
|
||||
};
|
||||
|
||||
export const PhoneActionCell = ({ phoneNumber, displayNumber, leadId: _leadId }: PhoneActionCellProps) => {
|
||||
const { isRegistered, isInCall } = useSip();
|
||||
const setCallState = useSetAtom(sipCallStateAtom);
|
||||
const setCallerNumber = useSetAtom(sipCallerNumberAtom);
|
||||
const setCallUcid = useSetAtom(sipCallUcidAtom);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [dialing, setDialing] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const touchTimer = useRef<number | null>(null);
|
||||
|
||||
// Close menu on click outside
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return;
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, [menuOpen]);
|
||||
|
||||
const handleCall = async () => {
|
||||
if (!isRegistered || isInCall || dialing) return;
|
||||
setMenuOpen(false);
|
||||
setDialing(true);
|
||||
setCallState('ringing-out');
|
||||
setCallerNumber(phoneNumber);
|
||||
setOutboundPending(true);
|
||||
const safetyTimer = setTimeout(() => setOutboundPending(false), 30000);
|
||||
|
||||
try {
|
||||
const result = await apiClient.post<{ ucid?: string }>('/api/ozonetel/dial', { phoneNumber });
|
||||
if (result?.ucid) setCallUcid(result.ucid);
|
||||
} catch {
|
||||
clearTimeout(safetyTimer);
|
||||
setCallState('idle');
|
||||
setCallerNumber(null);
|
||||
setOutboundPending(false);
|
||||
setCallUcid(null);
|
||||
notify.error('Dial Failed', 'Could not place the call');
|
||||
} finally {
|
||||
setDialing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSms = () => {
|
||||
setMenuOpen(false);
|
||||
window.open(`sms:+91${phoneNumber}`, '_self');
|
||||
};
|
||||
|
||||
const handleWhatsApp = () => {
|
||||
setMenuOpen(false);
|
||||
window.open(`https://wa.me/91${phoneNumber}`, '_blank');
|
||||
};
|
||||
|
||||
// Long-press for mobile
|
||||
const onTouchStart = () => {
|
||||
touchTimer.current = window.setTimeout(() => setMenuOpen(true), 500);
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (touchTimer.current) {
|
||||
clearTimeout(touchTimer.current);
|
||||
touchTimer.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const canCall = isRegistered && !isInCall && !dialing;
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center gap-1" ref={menuRef}>
|
||||
{/* Clickable phone number — calls directly */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCall}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchEnd={onTouchEnd}
|
||||
onContextMenu={(e) => { e.preventDefault(); setMenuOpen(true); }}
|
||||
disabled={!canCall}
|
||||
className={cx(
|
||||
'flex items-center gap-1.5 rounded-md px-1.5 py-1 text-sm transition duration-100 ease-linear',
|
||||
canCall
|
||||
? 'cursor-pointer text-brand-secondary hover:bg-brand-primary hover:text-brand-secondary'
|
||||
: 'cursor-default text-tertiary',
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPhone} className="size-3" />
|
||||
<span className="whitespace-nowrap">{displayNumber}</span>
|
||||
</button>
|
||||
|
||||
{/* Kebab menu trigger — desktop */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); setMenuOpen(!menuOpen); }}
|
||||
className="flex size-6 items-center justify-center rounded-md text-fg-quaternary opacity-0 group-hover/row:opacity-100 hover:text-fg-secondary hover:bg-primary_hover transition duration-100 ease-linear"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisVertical} className="size-3" />
|
||||
</button>
|
||||
|
||||
{/* Context menu */}
|
||||
{menuOpen && (
|
||||
<div className="absolute left-0 top-full z-50 mt-1 w-40 rounded-lg bg-primary shadow-lg ring-1 ring-secondary py-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCall}
|
||||
disabled={!canCall}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-secondary hover:bg-primary_hover disabled:text-disabled"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPhone} className="size-3.5 text-fg-success-secondary" />
|
||||
Call
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSms}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-secondary hover:bg-primary_hover"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCommentDots} className="size-3.5 text-fg-brand-secondary" />
|
||||
SMS
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleWhatsApp}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-secondary hover:bg-primary_hover"
|
||||
>
|
||||
<FontAwesomeIcon icon={faMessageDots} className="size-3.5 text-[#25D366]" />
|
||||
WhatsApp
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user