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,208 @@
import type { FC } from "react";
import { AlertCircle, CheckCircle, InfoCircle } from "@untitledui/icons";
import { Avatar } from "@/components/base/avatar/avatar";
import { Button } from "@/components/base/buttons/button";
import { CloseButton } from "@/components/base/buttons/close-button";
import { ProgressBar } from "@/components/base/progress-indicators/progress-indicators";
import { FeaturedIcon } from "@/components/foundations/featured-icon/featured-icon";
import { cx } from "@/utils/cx";
const iconMap = {
default: InfoCircle,
brand: InfoCircle,
gray: InfoCircle,
error: AlertCircle,
warning: AlertCircle,
success: CheckCircle,
};
interface IconNotificationProps {
title: string;
description: string;
confirmLabel?: string;
dismissLabel?: string;
hideDismissLabel?: boolean;
icon?: FC<{ className?: string }>;
color?: "default" | "brand" | "gray" | "error" | "warning" | "success";
progress?: number;
onClose?: () => void;
onConfirm?: () => void;
}
export const IconNotification = ({
title,
description,
confirmLabel,
dismissLabel = "Dismiss",
hideDismissLabel,
icon,
progress,
onClose,
onConfirm,
color = "default",
}: IconNotificationProps) => {
const showProgress = typeof progress === "number";
return (
<div className="relative z-[var(--z-index)] flex max-w-full flex-col gap-4 rounded-xl bg-primary_alt p-4 shadow-lg ring ring-secondary_alt xs:w-[var(--width)] xs:flex-row">
<FeaturedIcon
icon={icon || iconMap[color]}
color={color === "default" ? "gray" : color}
theme={color === "default" ? "modern" : "outline"}
size="md"
/>
<div className={cx("flex flex-1 flex-col gap-3 md:pr-8", color !== "default" && "md:pt-0.5", showProgress && "gap-4")}>
<div className="flex flex-col gap-1">
<p className="text-sm font-semibold text-fg-primary">{title}</p>
<p className="text-sm text-fg-secondary">{description}</p>
</div>
{showProgress && <ProgressBar labelPosition="bottom" value={progress} valueFormatter={(value) => `${value}% uploaded...`} />}
<div className="flex gap-3">
{!hideDismissLabel && (
<Button onClick={onClose} size="sm" color="link-gray">
{dismissLabel}
</Button>
)}
{confirmLabel && (
<Button onClick={onConfirm} size="sm" color="link-color">
{confirmLabel}
</Button>
)}
</div>
</div>
<div className="absolute top-2 right-2 flex items-center justify-center">
<CloseButton onClick={onClose} size="sm" label="Dismiss" />
</div>
</div>
);
};
interface AvatarNotificationProps {
name: string;
content: string;
avatar: string;
date: string;
confirmLabel: string;
dismissLabel?: string;
hideDismissLabel?: boolean;
icon?: FC<{ className?: string }>;
color?: "default" | "brand" | "gray" | "error" | "warning" | "success";
onClose?: () => void;
onConfirm?: () => void;
}
export const AvatarNotification = ({
name,
content,
avatar,
confirmLabel,
dismissLabel = "Dismiss",
hideDismissLabel,
date,
onClose,
onConfirm,
}: AvatarNotificationProps) => {
return (
<div className="relative z-[var(--z-index)] flex max-w-full flex-col items-start gap-4 rounded-xl bg-primary_alt p-4 shadow-lg ring ring-secondary_alt xs:w-[var(--width)] xs:flex-row">
<Avatar size="md" src={avatar} alt={name} status="online" />
<div className="flex flex-col gap-3 pr-8">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<p className="text-sm font-semibold text-fg-primary">{name}</p>
<span className="text-sm text-fg-quaternary">{date}</span>
</div>
<p className="text-sm text-fg-secondary">{content}</p>
</div>
<div className="flex gap-3">
{!hideDismissLabel && (
<Button onClick={onClose} size="sm" color="link-gray">
{dismissLabel}
</Button>
)}
{confirmLabel && (
<Button onClick={onConfirm} size="sm" color="link-color">
{confirmLabel}
</Button>
)}
</div>
</div>
<div className="absolute top-2 right-2 flex items-center justify-center">
<CloseButton onClick={onClose} size="sm" label="Dismiss" />
</div>
</div>
);
};
interface ImageNotificationProps {
title: string;
description: string;
confirmLabel: string;
dismissLabel?: string;
hideDismissLabel?: boolean;
imageMobile: string;
imageDesktop: string;
onClose?: () => void;
onConfirm?: () => void;
}
export const ImageNotification = ({
title,
description,
confirmLabel,
dismissLabel = "Dismiss",
hideDismissLabel,
imageMobile,
imageDesktop,
onClose,
onConfirm,
}: ImageNotificationProps) => {
return (
<div
style={
{
"--width": "496px",
} as React.CSSProperties
}
className="relative z-[var(--z-index)] flex max-w-full flex-col gap-3 rounded-xl bg-primary_alt p-4 shadow-lg max-md:ring-1 max-md:ring-secondary_alt xs:w-[var(--width)] xs:flex-row xs:gap-0 md:p-0"
>
<div className="-my-px hidden w-40 shrink-0 overflow-hidden rounded-l-xl outline-1 -outline-offset-1 outline-black/10 md:block">
<img aria-hidden="true" src={imageMobile} alt="Image Mobile" className="t size-full object-cover" />
</div>
<div className="flex flex-col gap-4 rounded-r-xl bg-primary_alt md:gap-3 md:p-4 md:pl-5 md:ring-1 md:ring-secondary_alt">
<div className="flex flex-col gap-1 pr-8">
<p className="text-sm font-semibold text-fg-primary">{title}</p>
<p className="text-sm text-fg-secondary">{description}</p>
</div>
<div className="h-40 w-full overflow-hidden rounded-md bg-secondary md:hidden">
<img src={imageDesktop} alt="Image Desktop" className="size-full object-cover" />
</div>
<div className="flex gap-3">
{!hideDismissLabel && (
<Button onClick={onClose} size="sm" color="link-gray">
{dismissLabel}
</Button>
)}
{confirmLabel && (
<Button onClick={onConfirm} size="sm" color="link-color">
{confirmLabel}
</Button>
)}
</div>
</div>
<div className="absolute top-2 right-2 flex items-center justify-center">
<CloseButton onClick={onClose} size="sm" label="Dismiss" />
</div>
</div>
);
};