AI_Manager_Dashboard / client /src /pages /dashboard.tsx
maitrang04's picture
Upload 91 files
d125a03 verified
import { useState, useCallback } from "react";
import { useQuery } from "@tanstack/react-query";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Skeleton } from "@/components/ui/skeleton";
import { DashboardHeader } from "@/components/dashboard/DashboardHeader";
import { AgentTile } from "@/components/dashboard/AgentTile";
import { CallDetailPanel } from "@/components/dashboard/CallDetailPanel";
import { CustomerProfile } from "@/components/dashboard/CustomerProfile";
import { PersonalizedOffers } from "@/components/dashboard/PersonalizedOffers";
import { InteractionSummary } from "@/components/dashboard/InteractionSummary";
import { KPISummary } from "@/components/dashboard/KPISummary";
import { AlertBanner } from "@/components/dashboard/AlertBanner";
import type { Agent, Call, Customer, Offer, TranscriptMessage, TimelineEvent, KPISummary as KPISummaryType, Alert } from "@shared/schema";
interface DashboardData {
agents: Agent[];
calls: Call[];
customers: Customer[];
offers: Offer[];
kpis: KPISummaryType;
}
export default function Dashboard() {
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const [selectedCallId, setSelectedCallId] = useState<string | null>(null);
const [filter, setFilter] = useState("all");
const [alerts, setAlerts] = useState<Alert[]>([]);
const { data, isLoading, refetch, isRefetching } = useQuery<DashboardData>({
queryKey: ["/api/dashboard"],
refetchInterval: 5000,
});
const { data: transcriptData } = useQuery<TranscriptMessage[]>({
queryKey: ["/api/calls", selectedCallId, "transcript"],
enabled: !!selectedCallId,
refetchInterval: 3000,
staleTime: 0,
});
const { data: timelineData } = useQuery<TimelineEvent[]>({
queryKey: ["/api/calls", selectedCallId, "timeline"],
enabled: !!selectedCallId,
refetchInterval: 5000,
staleTime: 0,
});
const handleAgentSelect = useCallback((agentId: string) => {
setSelectedAgentId(agentId);
}, []);
const handleCallSelect = useCallback((callId: string) => {
setSelectedCallId(callId);
const call = data?.calls.find(c => c.id === callId);
if (call) {
const agent = data?.agents.find(a => a.id === call.agentId);
if (agent) {
setSelectedAgentId(agent.id);
}
}
}, [data]);
const handleDismissAlert = useCallback((id: string) => {
setAlerts(prev => prev.filter(a => a.id !== id));
}, []);
const filteredAgents = data?.agents.filter(agent => {
if (filter === "all") return true;
return agent.status === filter;
}) || [];
const selectedCall = data?.calls.find(c => c.id === selectedCallId) || null;
const selectedCustomer = selectedCall
? data?.customers.find(c => c.id === selectedCall.customerId) || null
: null;
const customerOffers = selectedCustomer
? data?.offers.filter(o => o.customerId === selectedCustomer.id) || []
: [];
const getAgentCalls = (agentId: string): Call[] => {
return data?.calls.filter(c => c.agentId === agentId) || [];
};
if (isLoading) {
return <DashboardSkeleton />;
}
const activeAgents = data?.agents.filter(a => a.status === "active").length || 0;
const totalCalls = data?.calls.filter(c => c.status === "active").length || 0;
return (
<div className="flex flex-col min-h-screen bg-background">
<DashboardHeader
totalAgents={data?.agents.length || 0}
activeAgents={activeAgents}
totalCalls={totalCalls}
onRefresh={() => refetch()}
isRefreshing={isRefetching}
selectedFilter={filter}
onFilterChange={setFilter}
/>
<div className="flex flex-1 overflow-hidden">
<aside
className="w-80 border-r border-border bg-card/30 flex flex-col"
aria-label="AI Agents Overview"
data-testid="panel-agents"
>
<div className="px-4 py-3 border-b border-border">
<h2 className="text-lg font-semibold">AI Agents</h2>
<p className="text-xs text-muted-foreground">
{filteredAgents.length} agent{filteredAgents.length !== 1 ? "s" : ""} shown
</p>
</div>
<ScrollArea className="flex-1 px-4 py-4">
<div className="space-y-3">
{filteredAgents.map((agent) => (
<AgentTile
key={agent.id}
agent={agent}
calls={getAgentCalls(agent.id)}
isSelected={selectedAgentId === agent.id}
onSelect={handleAgentSelect}
onCallSelect={handleCallSelect}
selectedCallId={selectedCallId || undefined}
/>
))}
{filteredAgents.length === 0 && (
<div className="text-center py-8 text-sm text-muted-foreground">
No agents match the current filter
</div>
)}
</div>
</ScrollArea>
</aside>
<main className="flex-1 flex flex-col overflow-hidden" aria-label="Call Detail Interface" data-testid="panel-call-detail">
<CallDetailPanel
call={selectedCall}
transcript={transcriptData || []}
timeline={timelineData || []}
/>
</main>
<aside
className="w-96 border-l border-border bg-card/30 flex flex-col overflow-hidden"
aria-label="Customer & Offers"
data-testid="panel-customer"
>
<ScrollArea className="flex-1">
<div className="p-4 space-y-4">
<CustomerProfile customer={selectedCustomer} />
<PersonalizedOffers offers={customerOffers} />
<InteractionSummary call={selectedCall} />
</div>
</ScrollArea>
</aside>
</div>
<footer
className="border-t border-border bg-card/50 px-6 py-4"
aria-label="KPI Summary"
data-testid="panel-kpis"
>
<KPISummary kpis={data?.kpis || defaultKpis} />
</footer>
<AlertBanner alerts={alerts} onDismiss={handleDismissAlert} />
</div>
);
}
const defaultKpis: KPISummaryType = {
totalCalls: 0,
averageDuration: 0,
upsellSuccessRate: 0,
aiHandledPercentage: 0,
activeAgents: 0,
waitingAgents: 0,
issueAgents: 0,
};
function DashboardSkeleton() {
return (
<div className="flex flex-col min-h-screen bg-background">
<header className="border-b border-border p-6">
<div className="flex items-center gap-4 mb-4">
<Skeleton className="h-10 w-10 rounded-lg" />
<div>
<Skeleton className="h-7 w-48 mb-1" />
<Skeleton className="h-4 w-32" />
</div>
</div>
<div className="flex justify-center">
<div className="flex gap-2">
{Array.from({ length: 7 }).map((_, i) => (
<Skeleton key={i} className="h-14 w-20 rounded-lg" />
))}
</div>
</div>
</header>
<div className="flex flex-1">
<aside className="w-80 border-r border-border p-4">
<Skeleton className="h-6 w-24 mb-4" />
<div className="space-y-3">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-40 w-full rounded-lg" />
))}
</div>
</aside>
<main className="flex-1 p-6">
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Skeleton className="h-16 w-16 rounded-full mx-auto mb-4" />
<Skeleton className="h-6 w-32 mx-auto mb-2" />
<Skeleton className="h-4 w-48 mx-auto" />
</div>
</div>
</main>
<aside className="w-96 border-l border-border p-4 space-y-4">
<Skeleton className="h-48 w-full rounded-lg" />
<Skeleton className="h-64 w-full rounded-lg" />
<Skeleton className="h-32 w-full rounded-lg" />
</aside>
</div>
<footer className="border-t border-border p-4">
<div className="flex gap-4">
{Array.from({ length: 7 }).map((_, i) => (
<Skeleton key={i} className="h-20 flex-1 rounded-lg" />
))}
</div>
</footer>
</div>
);
}