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:
2026-03-18 18:33:36 +05:30
parent 61901eb8fb
commit 526ad18159
25 changed files with 1664 additions and 540 deletions

View 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;
};