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

@@ -1,25 +1,43 @@
import { useState } from 'react';
import { Phone01 } from '@untitledui/icons';
import { Button } from '@/components/base/buttons/button';
import { useSip } from '@/providers/sip-provider';
import { apiClient } from '@/lib/api-client';
import { notify } from '@/lib/toast';
interface ClickToCallButtonProps {
phoneNumber: string;
leadId?: string;
label?: string;
size?: 'sm' | 'md';
}
export const ClickToCallButton = ({ phoneNumber, label, size = 'sm' }: ClickToCallButtonProps) => {
const { makeCall, isRegistered, isInCall } = useSip();
export const ClickToCallButton = ({ phoneNumber, leadId, label, size = 'sm' }: ClickToCallButtonProps) => {
const { isRegistered, isInCall } = useSip();
const [dialing, setDialing] = useState(false);
const handleDial = async () => {
setDialing(true);
try {
await apiClient.post('/api/ozonetel/dial', { phoneNumber, leadId });
notify.success('Dialing', `Calling ${phoneNumber}...`);
} catch {
// apiClient.post already toasts the error
} finally {
setDialing(false);
}
};
return (
<Button
size={size}
color="primary"
iconLeading={Phone01}
onClick={() => makeCall(phoneNumber)}
isDisabled={!isRegistered || isInCall || phoneNumber === ''}
onClick={handleDial}
isDisabled={!isRegistered || isInCall || !phoneNumber || dialing}
isLoading={dialing}
>
{label ?? 'Call'}
{dialing ? 'Dialing...' : (label ?? 'Call')}
</Button>
);
};