Align ticket status colors across views
This commit is contained in:
parent
296e02cf0c
commit
6702811f4a
6 changed files with 110 additions and 89 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
{
|
||||
|
|
|
|||
85
src/lib/ticket-status-style.ts
Normal file
85
src/lib/ticket-status-style.ts
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue