172 lines
7.7 KiB
TypeScript
172 lines
7.7 KiB
TypeScript
"use client"
|
|
|
|
import Link from "next/link"
|
|
import { useState } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
import { IconArrowRight, IconPlayerPlayFilled } from "@tabler/icons-react"
|
|
import { useMutation, useQuery } from "convex/react"
|
|
import { api } from "@/convex/_generated/api"
|
|
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
|
import { useAuth } from "@/lib/auth-client"
|
|
import type { Ticket, TicketPlayContext, TicketQueueSummary } from "@/lib/schemas/ticket"
|
|
import type { Id } from "@/convex/_generated/dataModel"
|
|
import { mapTicketFromServer } from "@/lib/mappers/ticket"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Separator } from "@/components/ui/separator"
|
|
import { TicketPriorityPill } from "@/components/tickets/priority-pill"
|
|
import { TicketStatusBadge } from "@/components/tickets/status-badge"
|
|
import { Spinner } from "@/components/ui/spinner"
|
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
|
|
|
|
interface PlayNextTicketCardProps {
|
|
context?: TicketPlayContext
|
|
}
|
|
|
|
const queueBadgeClass = "inline-flex items-center rounded-full border border-slate-200 bg-white px-2.5 py-1 text-xs font-semibold text-neutral-700"
|
|
const startButtonClass = "inline-flex items-center gap-2 rounded-lg border border-black bg-[#00e8ff] px-3 py-2 text-sm font-semibold text-black transition hover:bg-[#00d6eb]"
|
|
const secondaryButtonClass = "inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-semibold text-neutral-700 hover:bg-slate-100"
|
|
|
|
export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {
|
|
const router = useRouter()
|
|
const { convexUserId, isStaff } = useAuth()
|
|
const queuesEnabled = Boolean(isStaff && convexUserId)
|
|
const queueSummaryResult = useQuery(
|
|
api.queues.summary,
|
|
queuesEnabled ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } : "skip"
|
|
)
|
|
const queueSummary: TicketQueueSummary[] = Array.isArray(queueSummaryResult) ? queueSummaryResult : []
|
|
const playNext = useMutation(api.tickets.playNext)
|
|
const [selectedQueueId, setSelectedQueueId] = useState<string | undefined>(undefined)
|
|
|
|
const nextTicketFromServer = useQuery(
|
|
api.tickets.list,
|
|
convexUserId
|
|
? {
|
|
tenantId: DEFAULT_TENANT_ID,
|
|
viewerId: convexUserId as Id<"users">,
|
|
status: undefined,
|
|
priority: undefined,
|
|
channel: undefined,
|
|
queueId: (selectedQueueId as Id<"queues">) || undefined,
|
|
limit: 1,
|
|
}
|
|
: "skip"
|
|
)?.[0]
|
|
const nextTicketUi: Ticket | null = nextTicketFromServer ? mapTicketFromServer(nextTicketFromServer as unknown) : null
|
|
const sanitizedNextTicket = nextTicketUi
|
|
? ({ ...nextTicketUi, lastTimelineEntry: nextTicketUi.lastTimelineEntry ?? undefined } as Ticket)
|
|
: null
|
|
|
|
const cardContext: TicketPlayContext | null =
|
|
context ??
|
|
(sanitizedNextTicket
|
|
? {
|
|
queue: {
|
|
id: "default",
|
|
name: "Geral",
|
|
pending: queueSummary.reduce((acc, item) => acc + item.pending, 0),
|
|
inProgress: queueSummary.reduce((acc, item) => acc + item.inProgress, 0),
|
|
paused: queueSummary.reduce((acc, item) => acc + item.paused, 0),
|
|
breached: queueSummary.reduce((acc, item) => acc + item.breached, 0),
|
|
},
|
|
nextTicket: sanitizedNextTicket,
|
|
}
|
|
: null)
|
|
|
|
if (!cardContext || !cardContext.nextTicket) {
|
|
return (
|
|
<Card className="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg font-semibold text-neutral-900">Fila sem tickets pendentes</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="text-sm text-neutral-600">
|
|
Nenhum ticket disponível no momento. Excelente trabalho!
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
const ticket = cardContext.nextTicket
|
|
|
|
return (
|
|
<Card className="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
|
<CardHeader className="flex flex-row items-center justify-between gap-2">
|
|
<CardTitle className="text-lg font-semibold text-neutral-900">
|
|
Próximo ticket • #{ticket.reference}
|
|
</CardTitle>
|
|
<TicketPriorityPill priority={ticket.priority} />
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col gap-4 text-sm text-neutral-700">
|
|
<div className="flex items-center justify-end gap-2">
|
|
<span className="text-xs uppercase tracking-wide text-neutral-500">Fila</span>
|
|
<Select value={selectedQueueId ?? "ALL"} onValueChange={(value) => setSelectedQueueId(value === "ALL" ? undefined : value)}>
|
|
<SelectTrigger className="h-8 w-[180px] rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]">
|
|
<SelectValue placeholder="Todas" />
|
|
</SelectTrigger>
|
|
<SelectContent className="rounded-lg border border-slate-200 bg-white text-neutral-800 shadow-sm">
|
|
<SelectItem value="ALL">Todas</SelectItem>
|
|
{queueSummary.map((queue) => (
|
|
<SelectItem key={queue.id} value={queue.id}>
|
|
{queue.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<h2 className="text-xl font-semibold text-neutral-900">{ticket.subject}</h2>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2 text-xs text-neutral-600">
|
|
<Badge className={queueBadgeClass}>{ticket.queue ?? "Sem fila"}</Badge>
|
|
<TicketStatusBadge status={ticket.status} />
|
|
<span className="font-medium text-neutral-900">Solicitante: {ticket.requester.name}</span>
|
|
</div>
|
|
<Separator className="bg-slate-200" />
|
|
<div className="flex flex-col gap-3 text-sm text-neutral-700">
|
|
<div className="flex items-center justify-between">
|
|
<span>Pendentes na fila</span>
|
|
<span className="font-semibold text-neutral-900">{cardContext.queue.pending}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span>Em andamento</span>
|
|
<span className="font-semibold text-neutral-900">{cardContext.queue.inProgress}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span>Pausados</span>
|
|
<span className="font-semibold text-neutral-900">{cardContext.queue.paused}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span>Fora do SLA</span>
|
|
<span className="font-semibold text-red-600">{cardContext.queue.breached}</span>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
className={startButtonClass}
|
|
onClick={async () => {
|
|
if (!convexUserId) return
|
|
const chosen = await playNext({ tenantId: DEFAULT_TENANT_ID, queueId: (selectedQueueId as Id<"queues">) || undefined, agentId: convexUserId as Id<"users"> })
|
|
if (chosen?.id) router.push(`/tickets/${chosen.id}`)
|
|
}}
|
|
>
|
|
{convexUserId ? (
|
|
<>
|
|
<IconPlayerPlayFilled className="size-4 text-black" /> Iniciar atendimento
|
|
</>
|
|
) : (
|
|
<>
|
|
<Spinner className="me-2" /> Carregando...
|
|
</>
|
|
)}
|
|
</Button>
|
|
<Button variant="ghost" asChild className={secondaryButtonClass}>
|
|
<Link href="/tickets">
|
|
Ver lista completa
|
|
<IconArrowRight className="size-4" />
|
|
</Link>
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|