Align ticket status colors across views

This commit is contained in:
codex-bot 2025-10-24 16:48:24 -03:00
parent 296e02cf0c
commit 6702811f4a
6 changed files with 110 additions and 89 deletions

View file

@ -11,20 +11,7 @@ import { useAuth } from "@/lib/auth-client"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { cn } from "@/lib/utils"
const statusLabel: Record<Ticket["status"], string> = {
PENDING: "Pendente",
AWAITING_ATTENDANCE: "Em andamento",
PAUSED: "Pausado",
RESOLVED: "Resolvido",
}
const statusTone: Record<Ticket["status"], string> = {
PENDING: "border border-slate-200 bg-slate-100 text-slate-700",
AWAITING_ATTENDANCE: "border border-sky-200 bg-sky-100 text-sky-700",
PAUSED: "border border-amber-200 bg-[#fff3c4] text-[#7a5901]",
RESOLVED: "border border-emerald-200 bg-emerald-100 text-emerald-700",
}
import { getTicketStatusBadgeClass, getTicketStatusLabel } from "@/lib/ticket-status-style"
const priorityLabel: Record<Ticket["priority"], string> = {
LOW: "Baixa",
@ -67,8 +54,8 @@ export function PortalTicketCard({ ticket }: PortalTicketCardProps) {
) : null}
</div>
<div className="flex flex-col items-end gap-2 text-right">
<Badge className={cn("rounded-full px-3 py-1 text-xs font-semibold uppercase", statusTone[ticket.status])}>
{statusLabel[ticket.status]}
<Badge className={cn("rounded-full px-3 py-1 text-xs font-semibold uppercase", getTicketStatusBadgeClass(ticket.status))}>
{getTicketStatusLabel(ticket.status)}
</Badge>
{!isCustomer ? (
<Badge className={cn("rounded-full px-3 py-1 text-xs font-semibold uppercase", priorityTone[ticket.priority])}>
@ -86,7 +73,7 @@ export function PortalTicketCard({ ticket }: PortalTicketCardProps) {
) : null}
<div className="flex flex-col gap-1">
<span className="text-xs uppercase tracking-wide text-neutral-500">Status</span>
<span className="font-medium text-neutral-800">{statusLabel[ticket.status]}</span>
<span className="font-medium text-neutral-800">{getTicketStatusLabel(ticket.status)}</span>
</div>
{ticket.assignee ? (
<div className="flex flex-col gap-1">

View file

@ -6,6 +6,7 @@ import type { TicketWithDetails } from "@/lib/schemas/ticket"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { cn } from "@/lib/utils"
import { getTicketStatusLabel, getTicketStatusSummaryTone } from "@/lib/ticket-status-style"
interface TicketDetailsPanelProps {
ticket: TicketWithDetails
@ -13,20 +14,6 @@ interface TicketDetailsPanelProps {
type SummaryTone = "default" | "info" | "warning" | "success" | "muted" | "danger"
const statusLabel: Record<TicketWithDetails["status"], string> = {
PENDING: "Pendente",
AWAITING_ATTENDANCE: "Em andamento",
PAUSED: "Pausado",
RESOLVED: "Resolvido",
}
const statusTone: Record<TicketWithDetails["status"], SummaryTone> = {
PENDING: "muted",
AWAITING_ATTENDANCE: "info",
PAUSED: "warning",
RESOLVED: "success",
}
const priorityLabel: Record<TicketWithDetails["priority"], string> = {
LOW: "Baixa",
MEDIUM: "Média",
@ -82,8 +69,8 @@ export function TicketDetailsPanel({ ticket }: TicketDetailsPanelProps) {
{
key: "status",
label: "Status",
value: statusLabel[ticket.status] ?? ticket.status,
tone: statusTone[ticket.status] ?? "default",
value: getTicketStatusLabel(ticket.status) ?? ticket.status,
tone: getTicketStatusSummaryTone(ticket.status) as SummaryTone,
},
{
key: "priority",

View file

@ -5,30 +5,17 @@ import { formatDistanceToNowStrict } from "date-fns"
import { ptBR } from "date-fns/locale"
import { LayoutGrid } from "lucide-react"
import type { Ticket, TicketStatus } from "@/lib/schemas/ticket"
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 { getTicketStatusChipClass, getTicketStatusLabel } from "@/lib/ticket-status-style"
type TicketsBoardProps = {
tickets: Ticket[]
}
const statusLabel: Record<TicketStatus, string> = {
PENDING: "Pendente",
AWAITING_ATTENDANCE: "Em andamento",
PAUSED: "Pausado",
RESOLVED: "Resolvido",
}
const statusChipClass: Record<TicketStatus, string> = {
PENDING: "bg-amber-100 text-amber-800 ring-1 ring-amber-200",
AWAITING_ATTENDANCE: "bg-sky-100 text-sky-700 ring-1 ring-sky-200",
PAUSED: "bg-violet-100 text-violet-700 ring-1 ring-violet-200",
RESOLVED: "bg-emerald-100 text-emerald-700 ring-1 ring-emerald-200",
}
function formatUpdated(date: Date) {
return formatDistanceToNowStrict(date, { addSuffix: true, locale: ptBR })
}
@ -71,10 +58,10 @@ export function TicketsBoard({ tickets }: TicketsBoardProps) {
<span
className={cn(
"inline-flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold transition",
statusChipClass[ticket.status],
getTicketStatusChipClass(ticket.status),
)}
>
{statusLabel[ticket.status]}
{getTicketStatusLabel(ticket.status)}
</span>
</div>
<span className="text-xs font-semibold uppercase tracking-wide text-neutral-400">

View file

@ -6,7 +6,7 @@ import { format, formatDistanceToNowStrict } from "date-fns"
import { ptBR } from "date-fns/locale"
import { type LucideIcon, Code, FileText, Mail, MessageCircle, MessageSquare, Phone } from "lucide-react"
import type { Ticket, TicketChannel, TicketStatus } from "@/lib/schemas/ticket"
import type { Ticket, TicketChannel } from "@/lib/schemas/ticket"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
@ -23,6 +23,7 @@ import {
import { PrioritySelect } from "@/components/tickets/priority-select"
import { cn } from "@/lib/utils"
import { deriveServerOffset, toServerTimestamp } from "@/components/tickets/ticket-timer.utils"
import { getTicketStatusLabel, getTicketStatusTextClass } from "@/lib/ticket-status-style"
const channelLabel: Record<TicketChannel, string> = {
EMAIL: "E-mail",
@ -48,20 +49,6 @@ const categoryChipClass = "inline-flex items-center gap-1 rounded-full bg-slate-
const tableRowClass =
"group border-b border-slate-100 text-sm transition-colors hover:bg-slate-100/70 last:border-none"
const statusLabel: Record<TicketStatus, string> = {
PENDING: "Pendente",
AWAITING_ATTENDANCE: "Em andamento",
PAUSED: "Pausado",
RESOLVED: "Resolvido",
}
const statusTone: Record<TicketStatus, string> = {
PENDING: "text-slate-700",
AWAITING_ATTENDANCE: "text-sky-700",
PAUSED: "text-violet-700",
RESOLVED: "text-emerald-700",
}
function formatDuration(ms?: number) {
if (!ms || ms <= 0) return "—"
const totalSeconds = Math.floor(ms / 1000)
@ -267,8 +254,11 @@ export function TicketsTable({ tickets }: TicketsTableProps) {
</TableCell>
<TableCell className={`${cellClass} pl-6 sm:pl-10 xl:pl-14`}>
<div className="flex flex-col gap-1">
<span className={cn("text-sm font-semibold break-words leading-tight max-w-[140px] sm:max-w-[180px] lg:max-w-[210px]", statusTone[ticket.status])}>
{statusLabel[ticket.status]}
<span className={cn(
"text-sm font-semibold break-words leading-tight max-w-[140px] sm:max-w-[180px] lg:max-w-[210px]",
getTicketStatusTextClass(ticket.status)
)}>
{getTicketStatusLabel(ticket.status)}
</span>
{ticket.metrics?.timeWaitingMinutes ? (
<span className="text-xs text-neutral-500">

View file

@ -25,6 +25,7 @@ import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { Input } from "@/components/ui/input"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { getTicketStatusDotClass, getTicketStatusLabel } from "@/lib/ticket-status-style"
import {
Bold,
Italic,
@ -115,20 +116,6 @@ type TicketMentionAttributes = Record<string, unknown>
const TICKET_MENTION_FALLBACK_STATUS = "PENDING"
const TICKET_MENTION_FALLBACK_PRIORITY = "MEDIUM"
const statusLabels: Record<string, string> = {
PENDING: "Pendente",
AWAITING_ATTENDANCE: "Em andamento",
PAUSED: "Pausado",
RESOLVED: "Resolvido",
}
const statusTone: Record<string, string> = {
PENDING: "bg-amber-400",
AWAITING_ATTENDANCE: "bg-sky-500",
PAUSED: "bg-violet-500",
RESOLVED: "bg-emerald-500",
}
function toPlainString(value: unknown): string {
if (value === null || value === undefined) return ""
return String(value)
@ -162,11 +149,11 @@ function updateTicketMentionNodeElements(elements: TicketMentionNodeElements, at
const displayedReference = referenceLabel ? `#${referenceLabel}` : "#"
const formattedSubject = normalized.subject ? formatMentionSubject(normalized.subject) : ""
const additionalClass = toPlainString(attrs.class ?? attrs.className)
const statusToneClass = statusTone[normalized.status] ?? "bg-slate-400"
const dotClass = getTicketStatusDotClass(normalized.status)
root.className = cn(TICKET_MENTION_BASE_CLASSES, additionalClass)
root.dataset.ticketMention = "true"
dotEl.className = cn(TICKET_MENTION_DOT_BASE_CLASSES, statusToneClass)
dotEl.className = cn(TICKET_MENTION_DOT_BASE_CLASSES, dotClass)
referenceEl.className = TICKET_MENTION_REF_CLASSES
subjectEl.className = TICKET_MENTION_SUBJECT_CLASSES
separatorEl.className = TICKET_MENTION_SEP_CLASSES
@ -244,8 +231,7 @@ function buildTicketMentionAnchorHtml(attrs: {
const url = attrs.url ?? ""
const displayedReference = reference || id
const formattedSubject = subject ? formatMentionSubject(subject) : ""
const dotToneClass = statusTone[status] ?? "bg-slate-400"
const dotClass = `${TICKET_MENTION_DOT_BASE_CLASSES} ${dotToneClass}`
const dotClass = `${TICKET_MENTION_DOT_BASE_CLASSES} ${getTicketStatusDotClass(status)}`
const title = formattedSubject ? `#${displayedReference}${formattedSubject}` : `#${displayedReference}`
return `<a data-ticket-mention="true" data-ticket-id="${escapeHtml(id)}" data-ticket-reference="${escapeHtml(reference)}" data-ticket-status="${escapeHtml(status)}" data-ticket-priority="${escapeHtml(priority)}" data-ticket-subject="${escapeHtml(subject)}" status="${escapeHtml(status)}" href="${escapeHtml(url)}" class="${escapeHtml(TICKET_MENTION_BASE_CLASSES)}" rel="noopener noreferrer" target="_self" title="${escapeHtml(title)}"><span class="${escapeHtml(dotClass)}"></span><span class="${escapeHtml(TICKET_MENTION_REF_CLASSES)}">#${escapeHtml(displayedReference)}</span><span class="${escapeHtml(TICKET_MENTION_SEP_CLASSES)}">•</span><span class="${escapeHtml(TICKET_MENTION_SUBJECT_CLASSES)}">${escapeHtml(formattedSubject)}</span></a>`
@ -424,8 +410,8 @@ function TicketMentionList({ items, command, onRegister }: TicketMentionSuggesti
<div className="max-h-72 min-w-[320px] space-y-1 overflow-y-auto p-2">
{items.map((item, index) => {
const isActive = index === selectedIndex
const status = statusLabels[item.status] ?? item.status
const statusDot = statusTone[item.status] ?? "bg-slate-400"
const status = getTicketStatusLabel(item.status)
const statusDot = getTicketStatusDotClass(item.status)
const priority = priorityLabels[item.priority] ?? item.priority
return (
<button
@ -579,8 +565,7 @@ const TicketMentionExtension = Mention.extend({
const priority = String(HTMLAttributes.priority ?? HTMLAttributes["data-ticket-priority"] ?? "MEDIUM").toUpperCase()
const href = String(HTMLAttributes.url ?? HTMLAttributes.href ?? "#")
const anchorClass = cn(TICKET_MENTION_BASE_CLASSES, toPlainString(HTMLAttributes.class ?? HTMLAttributes.className))
const statusToneClass = statusTone[status] ?? "bg-slate-400"
const dotClass = cn(TICKET_MENTION_DOT_BASE_CLASSES, statusToneClass)
const dotClass = cn(TICKET_MENTION_DOT_BASE_CLASSES, getTicketStatusDotClass(status))
return [
"a",
{

View file

@ -0,0 +1,85 @@
import type { TicketStatus } from "@/lib/schemas/ticket"
export type TicketStatusSummaryTone = "default" | "info" | "warning" | "success" | "muted" | "danger"
export type TicketStatusStyle = {
label: string
summaryTone: TicketStatusSummaryTone
chipClass: string
badgeClass: string
tableTextClass: string
dotClass: string
}
const FALLBACK_STATUS: TicketStatus = "PENDING"
const META: Record<TicketStatus, TicketStatusStyle> = {
PENDING: {
label: "Pendente",
summaryTone: "muted",
chipClass: "bg-slate-100 text-neutral-700 ring-1 ring-slate-200",
badgeClass: "border border-slate-200 bg-slate-100 text-neutral-700",
tableTextClass: "text-neutral-600",
dotClass: "bg-slate-400",
},
AWAITING_ATTENDANCE: {
label: "Em andamento",
summaryTone: "info",
chipClass: "bg-sky-100 text-sky-700 ring-1 ring-sky-200",
badgeClass: "border border-sky-200 bg-sky-100 text-sky-700",
tableTextClass: "text-sky-700",
dotClass: "bg-sky-500",
},
PAUSED: {
label: "Pausado",
summaryTone: "warning",
chipClass: "bg-amber-100 text-amber-800 ring-1 ring-amber-200",
badgeClass: "border border-amber-200 bg-amber-50 text-amber-700",
tableTextClass: "text-amber-700",
dotClass: "bg-amber-400",
},
RESOLVED: {
label: "Resolvido",
summaryTone: "success",
chipClass: "bg-emerald-100 text-emerald-700 ring-1 ring-emerald-200",
badgeClass: "border border-emerald-200 bg-emerald-50 text-emerald-700",
tableTextClass: "text-emerald-700",
dotClass: "bg-emerald-500",
},
}
function resolveMeta(status: TicketStatus | string | null | undefined): TicketStatusStyle {
if (!status) return META[FALLBACK_STATUS]
const normalized = status.toUpperCase() as TicketStatus
return META[normalized] ?? META[FALLBACK_STATUS]
}
export function getTicketStatusMeta(status: TicketStatus | string | null | undefined): TicketStatusStyle {
return resolveMeta(status)
}
export function getTicketStatusLabel(status: TicketStatus | string | null | undefined): string {
return resolveMeta(status).label
}
export function getTicketStatusChipClass(status: TicketStatus | string | null | undefined): string {
return resolveMeta(status).chipClass
}
export function getTicketStatusBadgeClass(status: TicketStatus | string | null | undefined): string {
return resolveMeta(status).badgeClass
}
export function getTicketStatusTextClass(status: TicketStatus | string | null | undefined): string {
return resolveMeta(status).tableTextClass
}
export function getTicketStatusDotClass(status: TicketStatus | string | null | undefined): string {
return resolveMeta(status).dotClass
}
export function getTicketStatusSummaryTone(status: TicketStatus | string | null | undefined): TicketStatusSummaryTone {
return resolveMeta(status).summaryTone
}
export const TICKET_STATUS_META = META