Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |