mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38:15 +00:00
feat: call desk redesign — 2-panel layout, collapsible sidebar, inline AI, ringtone
- Collapsible sidebar with Jotai atom (icon-only mode, persisted to localStorage) - 2-panel call desk: worklist (60%) + context panel (40%) with AI + Lead 360 tabs - Inline AI call prep card — known lead summary or unknown caller script - Active call card with compact Answer/Decline buttons - Worklist panel with human-readable labels, priority badges, click-to-select - Context panel auto-switches to Lead 360 when lead selected or call incoming - Browser ringtone via Web Audio API on incoming calls - Sonner + Untitled UI IconNotification for toast system - apiClient pattern: centralized post/get/graphql with auto-toast on errors - Remove duplicate avatar from top bar, hide floating widget on call desk - Fix Link routing in collapsed sidebar (was using <a> causing full page reload) - Fix GraphQL field names: adStatus→status, platformUrl needs subfield selection - Silent mode for DataProvider queries to prevent toast spam Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
110
src/components/call-desk/active-call-card.tsx
Normal file
110
src/components/call-desk/active-call-card.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPhone, faPhoneHangup, faMicrophone, faMicrophoneSlash, faPause, faPlay } from '@fortawesome/pro-duotone-svg-icons';
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { Badge } from '@/components/base/badges/badges';
|
||||
import { useSip } from '@/providers/sip-provider';
|
||||
import { formatPhone } from '@/lib/format';
|
||||
import type { Lead } from '@/types/entities';
|
||||
|
||||
interface ActiveCallCardProps {
|
||||
lead: Lead | null;
|
||||
callerPhone: string;
|
||||
}
|
||||
|
||||
const formatDuration = (seconds: number): string => {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => {
|
||||
const { callState, callDuration, isMuted, isOnHold, answer, reject, hangup, toggleMute, toggleHold } = useSip();
|
||||
|
||||
const firstName = lead?.contactName?.firstName ?? '';
|
||||
const lastName = lead?.contactName?.lastName ?? '';
|
||||
const fullName = `${firstName} ${lastName}`.trim();
|
||||
const phone = lead?.contactPhone?.[0];
|
||||
const phoneDisplay = phone ? formatPhone(phone) : callerPhone || 'Unknown';
|
||||
|
||||
if (callState === 'ringing-in') {
|
||||
return (
|
||||
<div className="rounded-xl bg-brand-primary p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 animate-ping rounded-full bg-brand-solid opacity-20" />
|
||||
<div className="relative flex size-10 items-center justify-center rounded-full bg-brand-solid">
|
||||
<FontAwesomeIcon icon={faPhone} className="size-4 text-white animate-bounce" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-brand-secondary">Incoming Call</p>
|
||||
<p className="text-lg font-bold text-primary">{fullName || phoneDisplay}</p>
|
||||
{fullName && <p className="text-sm text-secondary">{phoneDisplay}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<Button size="sm" color="primary" onClick={answer}>
|
||||
Answer
|
||||
</Button>
|
||||
<Button size="sm" color="tertiary-destructive" onClick={reject}>
|
||||
Decline
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (callState === 'active') {
|
||||
return (
|
||||
<div className="rounded-xl border border-brand bg-primary p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-full bg-success-solid">
|
||||
<FontAwesomeIcon icon={faPhone} className="size-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-primary">{fullName || phoneDisplay}</p>
|
||||
{fullName && <p className="text-xs text-tertiary">{phoneDisplay}</p>}
|
||||
</div>
|
||||
</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="primary-destructive"
|
||||
iconLeading={({ className }: { className?: string }) => (
|
||||
<FontAwesomeIcon icon={faPhoneHangup} className={className} />
|
||||
)}
|
||||
onClick={hangup}
|
||||
className="ml-auto"
|
||||
>
|
||||
End
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
Reference in New Issue
Block a user