style: refresh ticket ui components

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
esdrasrenan 2025-10-04 20:13:06 -03:00
parent 5c16ab75a6
commit 744d5933d4
16 changed files with 718 additions and 650 deletions

View file

@ -26,6 +26,16 @@ interface TicketHeaderProps {
ticket: TicketWithDetails
}
const cardClass = "space-y-4 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"
const referenceBadgeClass = "inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-semibold text-neutral-700"
const startButtonClass = "inline-flex items-center gap-1 rounded-lg border border-black bg-[#00e8ff] px-3 py-1.5 text-sm font-semibold text-black transition hover:bg-[#00d6eb]"
const pauseButtonClass = "inline-flex items-center gap-1 rounded-lg border border-black bg-black px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-black/90"
const editButtonClass = "inline-flex items-center gap-1 rounded-lg border border-black bg-black px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-black/90"
const selectTriggerClass = "h-8 w-[220px] rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]"
const smallSelectTriggerClass = "h-8 w-[180px] rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]"
const sectionLabelClass = "text-xs font-semibold uppercase tracking-wide text-neutral-500"
const sectionValueClass = "font-medium text-neutral-900"
export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
const { userId } = useAuth()
const changeAssignee = useMutation(api.tickets.changeAssignee)
@ -40,7 +50,10 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
const [editing, setEditing] = useState(false)
const [subject, setSubject] = useState(ticket.subject)
const [summary, setSummary] = useState(ticket.summary ?? "")
const dirty = useMemo(() => subject !== ticket.subject || (summary ?? "") !== (ticket.summary ?? ""), [subject, summary, ticket.subject, ticket.summary])
const dirty = useMemo(
() => subject !== ticket.subject || (summary ?? "") !== (ticket.summary ?? ""),
[subject, summary, ticket.subject, ticket.summary]
)
async function handleSave() {
if (!userId) return
@ -69,19 +82,16 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
const isPlaying = lastWork?.type === "WORK_STARTED"
return (
<div className="space-y-4 rounded-xl border bg-card p-6 shadow-sm">
<div className={cardClass}>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-2">
<div className="flex items-center gap-3">
<Badge variant="outline" className="rounded-full px-3 py-1 text-xs uppercase tracking-wide">
#{ticket.reference}
</Badge>
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-3">
<Badge className={referenceBadgeClass}>#{ticket.reference}</Badge>
<PrioritySelect ticketId={ticket.id} value={ticket.priority} />
<StatusSelect ticketId={ticket.id} value={status} />
<Button
size="sm"
variant={isPlaying ? "default" : "outline"}
className={isPlaying ? "bg-black text-white border-black" : "border-black text-black"}
className={isPlaying ? pauseButtonClass : startButtonClass}
onClick={async () => {
if (!userId) return
const next = await toggleWork({ ticketId: ticket.id as Id<"tickets">, actorId: userId as Id<"users"> })
@ -89,49 +99,65 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
else toast.success("Atendimento pausado", { id: "work" })
}}
>
{isPlaying ? (<><IconPlayerPause className="mr-1 size-4" /> Pausar</>) : (<><IconPlayerPlay className="mr-1 size-4" /> Iniciar</>)}
{isPlaying ? (
<>
<IconPlayerPause className="size-4 text-white" /> Pausar
</>
) : (
<>
<IconPlayerPlay className="size-4 text-black" /> Iniciar
</>
)}
</Button>
</div>
{editing ? (
<div className="space-y-2">
<Input value={subject} onChange={(e) => setSubject(e.target.value)} className="h-9 text-base font-semibold" />
<Input
value={subject}
onChange={(e) => setSubject(e.target.value)}
className="h-10 rounded-lg border border-slate-300 text-lg font-semibold text-neutral-900"
/>
<textarea
value={summary}
onChange={(e) => setSummary(e.target.value)}
rows={3}
className="w-full rounded-md border bg-background p-2 text-sm"
className="w-full rounded-lg border border-slate-300 bg-white p-3 text-sm text-neutral-800 shadow-sm"
placeholder="Adicione um resumo opcional"
/>
</div>
) : (
<>
<h1 className="break-words text-2xl font-semibold text-foreground">{subject}</h1>
{summary ? (
<p className="max-w-2xl text-sm text-muted-foreground">{summary}</p>
) : null}
</>
<div className="space-y-1">
<h1 className="break-words text-2xl font-semibold text-neutral-900">{subject}</h1>
{summary ? <p className="max-w-2xl text-sm text-neutral-600">{summary}</p> : null}
</div>
)}
<div className="ms-auto flex items-center gap-2">
<div className="flex items-center gap-2">
{editing ? (
<>
<Button variant="ghost" size="sm" onClick={handleCancel}>Cancelar</Button>
<Button size="sm" onClick={handleSave} disabled={!dirty}>Salvar</Button>
<Button variant="ghost" size="sm" className="text-sm font-semibold text-neutral-700" onClick={handleCancel}>
Cancelar
</Button>
<Button size="sm" className={startButtonClass} onClick={handleSave} disabled={!dirty}>
Salvar
</Button>
</>
) : (
<Button variant="outline" size="sm" onClick={() => setEditing(true)}>Editar</Button>
<Button size="sm" className={editButtonClass} onClick={() => setEditing(true)}>
Editar
</Button>
)}
<DeleteTicketDialog ticketId={ticket.id as Id<"tickets">} />
</div>
</div>
</div>
<Separator />
<div className="grid gap-4 text-sm text-muted-foreground sm:grid-cols-2 lg:grid-cols-3">
<Separator className="bg-slate-200" />
<div className="grid gap-4 text-sm text-neutral-600 sm:grid-cols-2 lg:grid-cols-3">
<div className="flex flex-col gap-1">
<span className="text-xs">Solicitante</span>
<span className="font-medium text-foreground">{ticket.requester.name}</span>
<span className={sectionLabelClass}>Solicitante</span>
<span className={sectionValueClass}>{ticket.requester.name}</span>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs">Responsável</span>
<span className={sectionLabelClass}>Responsável</span>
<Select
value={ticket.assignee?.id ?? ""}
onValueChange={async (value) => {
@ -145,57 +171,65 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
}
}}
>
<SelectTrigger className="h-8 w-[220px] border-black bg-white"><SelectValue placeholder="Selecionar" /></SelectTrigger>
<SelectContent>
{agents.map((a) => (
<SelectItem key={a._id} value={a._id}>{a.name}</SelectItem>
<SelectTrigger className={selectTriggerClass}>
<SelectValue placeholder="Selecionar" />
</SelectTrigger>
<SelectContent className="rounded-lg border border-slate-200 bg-white text-neutral-800 shadow-sm">
{agents.map((agent) => (
<SelectItem key={agent._id} value={agent._id}>
{agent.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs">Fila</span>
<span className={sectionLabelClass}>Fila</span>
<Select
value={ticket.queue ?? ""}
onValueChange={async (value) => {
if (!userId) return
const q = queues.find((qq) => qq.name === value)
if (!q) return
const queue = queues.find((item) => item.name === value)
if (!queue) return
toast.loading("Atualizando fila...", { id: "queue" })
try {
await changeQueue({ ticketId: ticket.id as Id<"tickets">, queueId: q.id as Id<"queues">, actorId: userId as Id<"users"> })
await changeQueue({ ticketId: ticket.id as Id<"tickets">, queueId: queue.id as Id<"queues">, actorId: userId as Id<"users"> })
toast.success("Fila atualizada!", { id: "queue" })
} catch {
toast.error("Não foi possível atualizar a fila.", { id: "queue" })
}
}}
>
<SelectTrigger className="h-8 w-[180px] border-black bg-white"><SelectValue placeholder="Selecionar" /></SelectTrigger>
<SelectContent>
{queues.map((q) => (
<SelectItem key={q.id} value={q.name}>{q.name}</SelectItem>
<SelectTrigger className={smallSelectTriggerClass}>
<SelectValue placeholder="Selecionar" />
</SelectTrigger>
<SelectContent className="rounded-lg border border-slate-200 bg-white text-neutral-800 shadow-sm">
{queues.map((queue) => (
<SelectItem key={queue.id} value={queue.name}>
{queue.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs">Atualizado em</span>
<span className="font-medium text-foreground">{format(ticket.updatedAt, "dd/MM/yyyy HH:mm", { locale: ptBR })}</span>
<span className={sectionLabelClass}>Atualizado em</span>
<span className={sectionValueClass}>{format(ticket.updatedAt, "dd/MM/yyyy HH:mm", { locale: ptBR })}</span>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs">Criado em</span>
<span className="font-medium text-foreground">{format(ticket.createdAt, "dd/MM/yyyy HH:mm", { locale: ptBR })}</span>
<span className={sectionLabelClass}>Criado em</span>
<span className={sectionValueClass}>{format(ticket.createdAt, "dd/MM/yyyy HH:mm", { locale: ptBR })}</span>
</div>
{ticket.dueAt ? (
<div className="flex flex-col gap-1">
<span className="text-xs">SLA até</span>
<span className="font-medium text-foreground">{format(ticket.dueAt, "dd/MM/yyyy HH:mm", { locale: ptBR })}</span>
<span className={sectionLabelClass}>SLA até</span>
<span className={sectionValueClass}>{format(ticket.dueAt, "dd/MM/yyyy HH:mm", { locale: ptBR })}</span>
</div>
) : null}
{ticket.slaPolicy ? (
<div className="flex flex-col gap-1">
<span className="text-xs">Política</span>
<span className="font-medium text-foreground">{ticket.slaPolicy.name}</span>
<span className={sectionLabelClass}>Política</span>
<span className={sectionValueClass}>{ticket.slaPolicy.name}</span>
</div>
) : null}
</div>