style: refresh ticket ui components
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
parent
5c16ab75a6
commit
744d5933d4
16 changed files with 718 additions and 650 deletions
|
|
@ -1,19 +1,18 @@
|
|||
import { format } from "date-fns"
|
||||
import type { ComponentType } from "react"
|
||||
import { ptBR } from "date-fns/locale"
|
||||
import {
|
||||
IconClockHour4,
|
||||
IconNote,
|
||||
IconSquareCheck,
|
||||
IconUserCircle,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
IconClockHour4,
|
||||
IconNote,
|
||||
IconSquareCheck,
|
||||
IconUserCircle,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
const timelineIcons: Record<string, ComponentType<{ className?: string }>> = {
|
||||
CREATED: IconUserCircle,
|
||||
STATUS_CHANGED: IconSquareCheck,
|
||||
|
|
@ -23,6 +22,7 @@ const timelineIcons: Record<string, ComponentType<{ className?: string }>> = {
|
|||
WORK_PAUSED: IconClockHour4,
|
||||
SUBJECT_CHANGED: IconNote,
|
||||
SUMMARY_CHANGED: IconNote,
|
||||
QUEUE_CHANGED: IconSquareCheck,
|
||||
}
|
||||
|
||||
const timelineLabels: Record<string, string> = {
|
||||
|
|
@ -36,70 +36,96 @@ const timelineLabels: Record<string, string> = {
|
|||
SUMMARY_CHANGED: "Resumo atualizado",
|
||||
QUEUE_CHANGED: "Fila alterada",
|
||||
}
|
||||
|
||||
interface TicketTimelineProps {
|
||||
ticket: TicketWithDetails
|
||||
}
|
||||
|
||||
|
||||
interface TicketTimelineProps {
|
||||
ticket: TicketWithDetails
|
||||
}
|
||||
|
||||
export function TicketTimeline({ ticket }: TicketTimelineProps) {
|
||||
return (
|
||||
<Card className="rounded-xl border bg-card shadow-sm">
|
||||
<CardContent className="space-y-6 px-4 pb-6">
|
||||
{ticket.timeline.map((entry, index) => {
|
||||
const Icon = timelineIcons[entry.type] ?? IconClockHour4
|
||||
const isLast = index === ticket.timeline.length - 1
|
||||
return (
|
||||
<div key={entry.id} className="relative pl-10">
|
||||
{!isLast && (
|
||||
<span className="absolute left-[17px] top-6 h-full w-px bg-border" aria-hidden />
|
||||
)}
|
||||
<span className="absolute left-0 top-0 flex size-8 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<Icon className="size-4" />
|
||||
</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Card className="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||
<CardContent className="space-y-5 px-4 pb-6">
|
||||
{ticket.timeline.map((entry, index) => {
|
||||
const Icon = timelineIcons[entry.type] ?? IconClockHour4
|
||||
const isLast = index === ticket.timeline.length - 1
|
||||
return (
|
||||
<div key={entry.id} className="relative pl-11">
|
||||
{!isLast && (
|
||||
<span className="absolute left-[14px] top-6 h-full w-px bg-slate-200" aria-hidden />
|
||||
)}
|
||||
<span className="absolute left-0 top-0 flex size-8 items-center justify-center rounded-full border border-slate-200 bg-white text-neutral-700 shadow-sm">
|
||||
<Icon className="size-4" />
|
||||
</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
<span className="text-sm font-semibold text-neutral-900">
|
||||
{timelineLabels[entry.type] ?? entry.type}
|
||||
</span>
|
||||
{entry.payload?.actorName ? (
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Avatar className="size-5">
|
||||
<span className="flex items-center gap-1 text-xs text-neutral-500">
|
||||
<Avatar className="size-5 border border-slate-200">
|
||||
<AvatarImage src={entry.payload?.actorAvatar as string | undefined} alt={String(entry.payload?.actorName ?? "")} />
|
||||
<AvatarFallback>
|
||||
{String(entry.payload?.actorName ?? '').split(' ').slice(0,2).map((p:string)=>p[0]).join('').toUpperCase()}
|
||||
{String(entry.payload?.actorName ?? "").split(" ").slice(0, 2).map((part: string) => part[0]).join("").toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
por {String(entry.payload?.actorName ?? '')}
|
||||
por {String(entry.payload?.actorName ?? "")}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-xs text-neutral-500">
|
||||
{format(entry.createdAt, "dd MMM yyyy HH:mm", { locale: ptBR })}
|
||||
</span>
|
||||
</div>
|
||||
{(() => {
|
||||
const p = (entry.payload || {}) as { toLabel?: string; to?: string; assigneeName?: string; assigneeId?: string; queueName?: string; queueId?: string; requesterName?: string; authorName?: string; authorId?: string; from?: string }
|
||||
const payload = (entry.payload || {}) as {
|
||||
toLabel?: string
|
||||
to?: string
|
||||
assigneeName?: string
|
||||
assigneeId?: string
|
||||
queueName?: string
|
||||
queueId?: string
|
||||
requesterName?: string
|
||||
authorName?: string
|
||||
authorId?: string
|
||||
from?: string
|
||||
}
|
||||
|
||||
let message: string | null = null
|
||||
if (entry.type === "STATUS_CHANGED" && (p.toLabel || p.to)) message = `Status alterado para ${p.toLabel || p.to}`
|
||||
if (entry.type === "ASSIGNEE_CHANGED" && (p.assigneeName || p.assigneeId)) message = `Responsável alterado${p.assigneeName ? ` para ${p.assigneeName}` : ""}`
|
||||
if (entry.type === "QUEUE_CHANGED" && (p.queueName || p.queueId)) message = `Fila alterada${p.queueName ? ` para ${p.queueName}` : ""}`
|
||||
if (entry.type === "CREATED" && (p.requesterName)) message = `Criado por ${p.requesterName}`
|
||||
if (entry.type === "COMMENT_ADDED" && (p.authorName || p.authorId)) message = `Comentário adicionado${p.authorName ? ` por ${p.authorName}` : ""}`
|
||||
if (entry.type === "SUBJECT_CHANGED" && (p.to || p.toLabel)) message = `Assunto alterado${p.to ? ` para “${p.to}”` : ""}`
|
||||
if (entry.type === "SUMMARY_CHANGED") message = `Resumo atualizado`
|
||||
if (entry.type === "STATUS_CHANGED" && (payload.toLabel || payload.to)) {
|
||||
message = "Status alterado para " + (payload.toLabel || payload.to)
|
||||
}
|
||||
if (entry.type === "ASSIGNEE_CHANGED" && (payload.assigneeName || payload.assigneeId)) {
|
||||
message = "Responsável alterado" + (payload.assigneeName ? " para " + payload.assigneeName : "")
|
||||
}
|
||||
if (entry.type === "QUEUE_CHANGED" && (payload.queueName || payload.queueId)) {
|
||||
message = "Fila alterada" + (payload.queueName ? " para " + payload.queueName : "")
|
||||
}
|
||||
if (entry.type === "CREATED" && payload.requesterName) {
|
||||
message = "Criado por " + payload.requesterName
|
||||
}
|
||||
if (entry.type === "COMMENT_ADDED" && (payload.authorName || payload.authorId)) {
|
||||
message = "Comentário adicionado" + (payload.authorName ? " por " + payload.authorName : "")
|
||||
}
|
||||
if (entry.type === "SUBJECT_CHANGED" && (payload.to || payload.toLabel)) {
|
||||
message = "Assunto alterado" + (payload.to ? " para \"" + payload.to + "\"" : "")
|
||||
}
|
||||
if (entry.type === "SUMMARY_CHANGED") {
|
||||
message = "Resumo atualizado"
|
||||
}
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed bg-card px-3 py-2 text-sm text-muted-foreground">
|
||||
<div className="rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-neutral-600">
|
||||
{message}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Separator className="bg-slate-200" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue