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
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue