feat: ajustar board de tickets
This commit is contained in:
parent
e9a8bd6b9b
commit
d23987eda8
7 changed files with 434 additions and 429 deletions
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue