Improve admin actions and ticket board layout

This commit is contained in:
codex-bot 2025-10-24 14:04:12 -03:00
parent 92ac0fafc6
commit 12a6d231fa
4 changed files with 59 additions and 96 deletions

View file

@ -5,10 +5,11 @@ import { formatDistanceToNowStrict } from "date-fns"
import { ptBR } from "date-fns/locale"
import { LayoutGrid } from "lucide-react"
import type { Ticket, TicketPriority, TicketStatus } from "@/lib/schemas/ticket"
import type { Ticket, TicketStatus } 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"
type TicketsBoardProps = {
tickets: Ticket[]
@ -28,20 +29,6 @@ const statusChipClass: Record<TicketStatus, string> = {
RESOLVED: "bg-emerald-100 text-emerald-700 ring-1 ring-emerald-200",
}
const priorityLabel: Record<TicketPriority, string> = {
LOW: "Baixa",
MEDIUM: "Média",
HIGH: "Alta",
URGENT: "Urgente",
}
const priorityChipClass: Record<TicketPriority, string> = {
LOW: "bg-slate-100 text-slate-700",
MEDIUM: "bg-sky-100 text-sky-700",
HIGH: "bg-amber-100 text-amber-800",
URGENT: "bg-rose-100 text-rose-700",
}
function formatUpdated(date: Date) {
return formatDistanceToNowStrict(date, { addSuffix: true, locale: ptBR })
}
@ -71,84 +58,63 @@ export function TicketsBoard({ tickets }: TicketsBoardProps) {
<Link
key={ticket.id}
href={`/tickets/${ticket.id}`}
className="group block h-full rounded-3xl border border-slate-200 bg-white p-4 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"
className="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"
>
<div className="flex items-start justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-start justify-between gap-4">
<div className="flex flex-wrap items-center gap-3">
<Badge
variant="outline"
className="border-slate-200 bg-slate-100 px-2.5 py-1 text-[11px] font-semibold text-neutral-700"
className="border-slate-200 bg-slate-100 px-3.5 py-1.5 text-xs font-semibold text-neutral-700"
>
#{ticket.reference}
</Badge>
<span
className={cn(
"inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[11px] font-semibold transition",
"inline-flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold transition",
statusChipClass[ticket.status],
)}
>
{statusLabel[ticket.status]}
</span>
</div>
<span className="text-[11px] font-semibold uppercase tracking-wide text-neutral-400">
<span className="text-xs font-semibold uppercase tracking-wide text-neutral-400">
{formatUpdated(ticket.updatedAt)}
</span>
</div>
<h3 className="mt-3 line-clamp-2 text-sm font-semibold text-neutral-900">
<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-2 text-xs text-neutral-600">
<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-2.5 py-0.5 text-[11px] font-medium text-neutral-700">
<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>
<span
className={cn(
"rounded-full px-2.5 py-0.5 text-[11px] font-semibold shadow-sm",
priorityChipClass[ticket.priority],
)}
>
{priorityLabel[ticket.priority]}
</span>
<TicketPriorityPill
priority={ticket.priority}
className="h-7 gap-1.5 px-3.5 text-xs shadow-sm"
/>
</div>
<dl className="mt-4 space-y-2 text-xs text-neutral-600">
<div className="flex items-center justify-between gap-3">
<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>
<div className="flex items-center justify-between gap-3">
<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-center justify-between gap-3">
<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>
{ticket.tags.length > 0 ? (
<div className="mt-4 flex flex-wrap items-center gap-2">
{ticket.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-neutral-600"
>
{tag}
</span>
))}
{ticket.tags.length > 3 ? (
<span className="text-[11px] font-semibold text-neutral-400">
+{ticket.tags.length - 3}
</span>
) : null}
</div>
) : null}
</Link>
))}
</div>

View file

@ -35,27 +35,32 @@ export function TicketsView({ initialFilters }: TicketsViewProps = {}) {
setFilters(mergedInitialFilters)
}, [mergedInitialFilters])
const { session, convexUserId, isStaff } = useAuth()
const userId = session?.user?.id ?? null
const tenantId = session?.user?.tenantId ?? DEFAULT_TENANT_ID
const viewModeStorageKey = useMemo(() => {
const userKey = userId ?? (convexUserId ? String(convexUserId) : "anonymous")
return `tickets:view-mode:${tenantId}:${userKey}`
}, [tenantId, userId, convexUserId])
useEffect(() => {
try {
const stored = localStorage.getItem("tickets:view-mode")
const stored = localStorage.getItem(viewModeStorageKey)
if (stored === "table" || stored === "board") {
setViewMode(stored)
}
} catch {
// ignore
}
}, [])
}, [viewModeStorageKey])
useEffect(() => {
try {
localStorage.setItem("tickets:view-mode", viewMode)
localStorage.setItem(viewModeStorageKey, viewMode)
} catch {
// ignore
}
}, [viewMode])
const { session, convexUserId, isStaff } = useAuth()
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
}, [viewMode, viewModeStorageKey])
useDefaultQueues(tenantId)