feat: ajustar board de tickets

This commit is contained in:
Esdras Renan 2025-10-27 14:50:17 -03:00
parent e9a8bd6b9b
commit d23987eda8
7 changed files with 434 additions and 429 deletions

View file

@ -40,10 +40,12 @@ export function PrioritySelect({
ticketId,
value,
className,
badgeClassName,
}: {
ticketId: string
value: TicketPriority
className?: string
badgeClassName?: string
}) {
const updatePriority = useMutation(api.tickets.updatePriority)
const [priority, setPriority] = useState<TicketPriority>(value)
@ -69,7 +71,9 @@ export function PrioritySelect({
>
<SelectTrigger className={cn(headerTriggerClass, className)} aria-label="Atualizar prioridade">
<SelectValue asChild>
<Badge className={cn(priorityBadgeClass, priorityStyles[priority]?.badgeClass)}>
<Badge
className={cn(priorityBadgeClass, priorityStyles[priority]?.badgeClass, badgeClassName)}
>
<PriorityIcon value={priority} />
{priorityStyles[priority]?.label ?? priority}
<ChevronDown className="size-3 text-current transition group-data-[state=open]:rotate-180" />

View file

@ -10,7 +10,7 @@ import type { Ticket } from "@/lib/schemas/ticket"
import { Badge } from "@/components/ui/badge"
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty"
import { cn } from "@/lib/utils"
import { TicketPriorityPill } from "@/components/tickets/priority-pill"
import { PrioritySelect } from "@/components/tickets/priority-select"
import { getTicketStatusChipClass, getTicketStatusLabel } from "@/lib/ticket-status-style"
type TicketsBoardProps = {
@ -57,6 +57,17 @@ function formatUpdated(date: Date | number | string, now: number) {
return formatDistanceStrict(timestamp, now, { addSuffix: true, locale: ptBR })
}
function formatQueueLabel(queue?: string | null) {
if (!queue) {
return { label: "Sem fila", title: "Sem fila" }
}
const normalized = queue.trim().toLowerCase()
if (normalized.startsWith("laboratorio")) {
return { label: "Lab", title: queue }
}
return { label: queue, title: queue }
}
export function TicketsBoard({ tickets, enteringIds }: TicketsBoardProps) {
const [now, setNow] = useState(() => Date.now())
@ -103,73 +114,118 @@ export function TicketsBoard({ tickets, enteringIds }: TicketsBoardProps) {
}
return (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
{tickets.map((ticket) => {
const isEntering = enteringIds?.has(ticket.id)
const queueDisplay = formatQueueLabel(ticket.queue)
const updatedTimestamp = getTimestamp(ticket.updatedAt)
const updatedAbsoluteLabel = updatedTimestamp
? new Date(updatedTimestamp).toLocaleString("pt-BR", {
dateStyle: "short",
timeStyle: "short",
})
: "—"
return (
<Link
key={ticket.id}
href={`/tickets/${ticket.id}`}
className={cn(
"group block h-full rounded-3xl border border-slate-200 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:border-slate-300 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-300",
isEntering ? "recent-ticket-enter" : ""
"group flex h-full flex-col rounded-3xl border border-slate-200 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:border-slate-300 hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-300",
isEntering ? "recent-ticket-enter" : "",
)}
>
<div className="flex items-start justify-between gap-4">
<div className="flex flex-wrap items-center gap-3">
<div className="flex w-full flex-col gap-3">
<div className="flex w-full items-center justify-between gap-4">
<Badge
variant="outline"
className="border-slate-200 bg-slate-100 px-3.5 py-1.5 text-xs font-semibold text-neutral-700"
className="rounded-full border-slate-200 bg-white px-4 py-2 text-sm font-semibold tracking-tight text-neutral-700"
>
#{ticket.reference}
</Badge>
<span className="text-right text-sm font-semibold text-neutral-900" title={queueDisplay.title}>
{queueDisplay.label}
</span>
</div>
<div className="flex w-full items-center justify-between gap-4">
<span
className={cn(
"inline-flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold transition",
"inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-semibold transition",
getTicketStatusChipClass(ticket.status),
)}
>
{getTicketStatusLabel(ticket.status)}
</span>
<div
className="relative z-[1] flex justify-end"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}
>
<PrioritySelect
className="min-w-[7rem]"
badgeClassName="h-9 px-4 shadow-sm"
ticketId={ticket.id}
value={ticket.priority}
/>
</div>
</div>
<span className="text-xs font-semibold uppercase tracking-wide text-neutral-400">
{formatUpdated(ticket.updatedAt, now)}
</span>
</div>
<h3 className="mt-5 line-clamp-2 text-lg font-semibold text-neutral-900">
{ticket.subject || "Sem assunto"}
</h3>
<div className="mt-3 flex flex-wrap items-center gap-3 text-sm text-neutral-600">
<span className="font-medium text-neutral-500">Fila:</span>
<span className="rounded-full bg-slate-100 px-3.5 py-0.5 text-xs font-medium text-neutral-700">
{ticket.queue ?? "Sem fila"}
</span>
<span className="font-medium text-neutral-500">Prioridade:</span>
<TicketPriorityPill
priority={ticket.priority}
className="h-7 gap-1.5 px-3.5 text-xs shadow-sm"
/>
<div className="mt-4 rounded-2xl border border-slate-200 bg-white px-4 py-3 text-center shadow-sm">
<h3 className="line-clamp-2 text-lg font-semibold text-neutral-900">
{ticket.subject || "Sem assunto"}
</h3>
</div>
<dl className="mt-6 space-y-2.5 text-sm text-neutral-600">
<div className="flex items-start justify-between gap-4">
<dt className="font-medium text-neutral-500">Empresa</dt>
<dd className="truncate text-right text-neutral-700">
{ticket.company?.name ?? "Sem empresa"}
</dd>
<div className="mt-4 flex flex-1 flex-col gap-5 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
<dl className="grid grid-cols-1 gap-4 text-sm text-neutral-600 sm:grid-cols-2">
<div className="flex flex-col gap-1">
<dt className="text-xs font-semibold uppercase tracking-wide text-neutral-400">
Empresa
</dt>
<dd className="text-neutral-700">
{ticket.company?.name ?? "Sem empresa"}
</dd>
</div>
<div className="flex flex-col gap-1">
<dt className="text-xs font-semibold uppercase tracking-wide text-neutral-400">
Responsável
</dt>
<dd className="text-neutral-700">
{ticket.assignee?.name ?? "Sem responsável"}
</dd>
</div>
<div className="flex flex-col gap-1">
<dt className="text-xs font-semibold uppercase tracking-wide text-neutral-400">
Solicitante
</dt>
<dd className="text-neutral-700">
{ticket.requester?.name ?? ticket.requester?.email ?? "—"}
</dd>
</div>
<div className="flex flex-col gap-1">
<dt className="text-xs font-semibold uppercase tracking-wide text-neutral-400">
Criado em
</dt>
<dd className="text-neutral-700">
{(() => {
const createdTimestamp = getTimestamp(ticket.createdAt)
return createdTimestamp
? new Date(createdTimestamp).toLocaleDateString("pt-BR")
: "—"
})()}
</dd>
</div>
</dl>
<div className="mt-auto flex items-center justify-center border-t border-slate-200 pt-4 text-sm text-neutral-600 text-center">
<span className="text-neutral-700">
Categoria:{" "}
<span className="font-semibold text-neutral-900">{ticket.category?.name ?? "Sem categoria"}</span>
</span>
</div>
<div className="flex items-start justify-between gap-4">
<dt className="font-medium text-neutral-500">Responsável</dt>
<dd className="truncate text-right text-neutral-700">
{ticket.assignee?.name ?? "Sem responsável"}
</dd>
</div>
<div className="flex items-start justify-between gap-4">
<dt className="font-medium text-neutral-500">Solicitante</dt>
<dd className="truncate text-right text-neutral-700">
{ticket.requester?.name ?? ticket.requester?.email ?? "—"}
</dd>
</div>
</dl>
</div>
</Link>
)
})}