Spaces:
Sleeping
Sleeping
| import { Card, CardContent, CardHeader } from "@/components/ui/card"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { StatusIndicator } from "./StatusIndicator"; | |
| import { Phone, User } from "lucide-react"; | |
| import { cn } from "@/lib/utils"; | |
| import type { Agent, Call } from "@shared/schema"; | |
| interface AgentTileProps { | |
| agent: Agent; | |
| calls: Call[]; | |
| isSelected?: boolean; | |
| onSelect: (agentId: string) => void; | |
| onCallSelect: (callId: string) => void; | |
| selectedCallId?: string; | |
| } | |
| export function AgentTile({ | |
| agent, | |
| calls, | |
| isSelected = false, | |
| onSelect, | |
| onCallSelect, | |
| selectedCallId | |
| }: AgentTileProps) { | |
| const activeCalls = calls.filter(c => c.status === "active").slice(0, 5); | |
| const status = agent.status as "active" | "waiting" | "issue"; | |
| return ( | |
| <Card | |
| className={cn( | |
| "hover-elevate cursor-pointer transition-all duration-200", | |
| isSelected && "ring-2 ring-primary ring-offset-2" | |
| )} | |
| onClick={() => onSelect(agent.id)} | |
| role="button" | |
| tabIndex={0} | |
| aria-label={`Agent ${agent.name}, ${agent.activeCallCount} active calls, status ${status}`} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter" || e.key === " ") { | |
| e.preventDefault(); | |
| onSelect(agent.id); | |
| } | |
| }} | |
| data-testid={`agent-tile-${agent.id}`} | |
| > | |
| <CardHeader className="flex flex-row items-center justify-between gap-2 pb-2 space-y-0"> | |
| <div className="flex items-center gap-2"> | |
| <div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10"> | |
| <User className="h-4 w-4 text-primary" aria-hidden="true" /> | |
| </div> | |
| <div> | |
| <h3 className="text-sm font-semibold leading-none">{agent.name}</h3> | |
| <p className="text-xs font-mono text-muted-foreground mt-0.5">ID: {agent.id.slice(0, 8)}</p> | |
| </div> | |
| </div> | |
| <StatusIndicator status={status} size="md" /> | |
| </CardHeader> | |
| <CardContent className="space-y-3"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-xs text-muted-foreground">Active Calls</span> | |
| <Badge variant="secondary" className="text-xs"> | |
| {agent.activeCallCount} | |
| </Badge> | |
| </div> | |
| {activeCalls.length > 0 && ( | |
| <div className="space-y-2"> | |
| {activeCalls.map((call) => ( | |
| <div | |
| key={call.id} | |
| className={cn( | |
| "flex items-center justify-between p-2 rounded-md bg-muted/50 hover-elevate cursor-pointer", | |
| selectedCallId === call.id && "ring-1 ring-primary" | |
| )} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| onCallSelect(call.id); | |
| }} | |
| role="button" | |
| tabIndex={0} | |
| aria-label={`Call ${call.id.slice(0, 8)}, duration ${formatDuration(call.duration)}`} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter" || e.key === " ") { | |
| e.stopPropagation(); | |
| e.preventDefault(); | |
| onCallSelect(call.id); | |
| } | |
| }} | |
| data-testid={`call-tile-${call.id}`} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <Phone className="h-3 w-3 text-muted-foreground" aria-hidden="true" /> | |
| <span className="text-xs font-mono">{call.id.slice(0, 8)}</span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-xs text-muted-foreground font-mono"> | |
| {formatDuration(call.duration)} | |
| </span> | |
| <StatusIndicator status={call.status === "active" ? "active" : "waiting"} size="sm" /> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {activeCalls.length === 0 && ( | |
| <div className="text-center py-3 text-xs text-muted-foreground"> | |
| No active calls | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| ); | |
| } | |
| function formatDuration(seconds: number): string { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = seconds % 60; | |
| return `${mins}:${secs.toString().padStart(2, "0")}`; | |
| } | |