ui: header cleanup (edit icon, time tooltip), delete button style; filters: server-side assignee + company mapping; UX: toasts on save/clear default filter

This commit is contained in:
codex-bot 2025-10-20 16:40:27 -03:00
parent f5b3abd277
commit 9b31a47f82
4 changed files with 38 additions and 28 deletions

View file

@ -48,9 +48,9 @@ export function DeleteTicketDialog({ ticketId }: { ticketId: Id<"tickets"> }) {
<Button <Button
size="icon" size="icon"
aria-label="Excluir ticket" aria-label="Excluir ticket"
className="h-9 w-9 rounded-lg border border-transparent bg-transparent text-[#ef4444] transition hover:border-[#fecaca] hover:bg-[#fee2e2] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#fecaca]/50" className="h-9 w-9 rounded-lg border border-rose-300 bg-white text-rose-600 transition hover:bg-rose-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-200"
> >
<Trash2 className="size-4 text-current" /> <Trash2 className="size-4" />
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-md"> <DialogContent className="max-w-md">

View file

@ -3,7 +3,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { format, formatDistanceToNow } from "date-fns" import { format, formatDistanceToNow } from "date-fns"
import { ptBR } from "date-fns/locale" import { ptBR } from "date-fns/locale"
import { IconClock, IconDownload, IconInfoCircle, IconPlayerPause, IconPlayerPlay } from "@tabler/icons-react" import { IconClock, IconDownload, IconInfoCircle, IconPlayerPause, IconPlayerPlay, IconPencil } from "@tabler/icons-react"
import { useMutation, useQuery } from "convex/react" import { useMutation, useQuery } from "convex/react"
import { toast } from "sonner" import { toast } from "sonner"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
@ -617,10 +617,16 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
return ( return (
<div className={cardClass}> <div className={cardClass}>
<div className="absolute right-6 top-6 flex items-center gap-3"> <div className="absolute right-6 top-6 flex items-center gap-3">
{!editing ? ( {!editing ? (
<Button size="sm" className={editButtonClass} onClick={() => setEditing(true)}> <Button
Editar size="icon"
aria-label="Editar"
className="inline-flex items-center justify-center rounded-lg border border-slate-200 bg-white text-neutral-800 hover:bg-slate-50"
onClick={() => setEditing(true)}
title="Editar"
>
<IconPencil className="size-5" />
</Button> </Button>
) : null} ) : null}
<Button <Button
@ -635,28 +641,22 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
{exportingPdf ? <Spinner className="size-4 text-neutral-700" /> : <IconDownload className="size-5" />} {exportingPdf ? <Spinner className="size-4 text-neutral-700" /> : <IconDownload className="size-5" />}
</Button> </Button>
{workSummary ? ( {workSummary ? (
<div className="flex items-center gap-2"> <Tooltip>
<Badge className="inline-flex h-9 items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700"> <TooltipTrigger asChild>
<IconClock className="size-4 text-neutral-700" /> Tempo total: {formattedTotalWorked} <Badge
</Badge> className="inline-flex h-9 cursor-help items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700"
<Tooltip> title="Tempo total de atendimento"
<TooltipTrigger asChild> >
<button <IconClock className="size-4 text-neutral-700" /> Tempo total: {formattedTotalWorked}
type="button" </Badge>
className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-slate-200 bg-white text-neutral-600 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#00d6eb]/30" </TooltipTrigger>
aria-label="Ver detalhamento das horas internas e externas" <TooltipContent className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-medium text-neutral-700 shadow-lg">
> <div className="flex flex-col gap-1">
<IconInfoCircle className="size-4" /> <span>Horas internas: {formatDuration(internalWorkedMs)}</span>
</button> <span>Horas externas: {formatDuration(externalWorkedMs)}</span>
</TooltipTrigger> </div>
<TooltipContent className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-medium text-neutral-700 shadow-lg"> </TooltipContent>
<div className="flex flex-col gap-1"> </Tooltip>
<span>Horas internas: {formatDuration(internalWorkedMs)}</span>
<span>Horas externas: {formatDuration(externalWorkedMs)}</span>
</div>
</TooltipContent>
</Tooltip>
</div>
) : null} ) : null}
<DeleteTicketDialog ticketId={ticket.id as Id<"tickets">} /> <DeleteTicketDialog ticketId={ticket.id as Id<"tickets">} />
</div> </div>

View file

@ -1,6 +1,7 @@
"use client" "use client"
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { toast } from "sonner"
import { useQuery } from "convex/react" import { useQuery } from "convex/react"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
@ -96,6 +97,7 @@ export function TicketsView({ initialFilters }: TicketsViewProps = {}) {
try { try {
const key = `tickets:filters:${tenantId}:${String(convexUserId)}` const key = `tickets:filters:${tenantId}:${String(convexUserId)}`
localStorage.setItem(key, JSON.stringify(filters)) localStorage.setItem(key, JSON.stringify(filters))
toast.success("Filtro salvo como padrão")
} catch { } catch {
// ignore // ignore
} }
@ -106,6 +108,7 @@ export function TicketsView({ initialFilters }: TicketsViewProps = {}) {
try { try {
const key = `tickets:filters:${tenantId}:${String(convexUserId)}` const key = `tickets:filters:${tenantId}:${String(convexUserId)}`
localStorage.removeItem(key) localStorage.removeItem(key)
toast.success("Padrão de filtro limpo")
} catch { } catch {
// ignore // ignore
} }

View file

@ -42,6 +42,10 @@ const serverTicketSchema = z.object({
queue: z.string().nullable(), queue: z.string().nullable(),
requester: serverUserSchema, requester: serverUserSchema,
assignee: serverUserSchema.nullable(), assignee: serverUserSchema.nullable(),
company: z
.object({ id: z.string(), name: z.string(), isAvulso: z.boolean().optional() })
.optional()
.nullable(),
slaPolicy: z.any().nullable().optional(), slaPolicy: z.any().nullable().optional(),
dueAt: z.number().nullable().optional(), dueAt: z.number().nullable().optional(),
firstResponseAt: z.number().nullable().optional(), firstResponseAt: z.number().nullable().optional(),
@ -131,6 +135,9 @@ export function mapTicketFromServer(input: unknown) {
const ui = { const ui = {
...s, ...s,
status: normalizeTicketStatus(s.status), status: normalizeTicketStatus(s.status),
company: s.company
? { id: s.company.id, name: s.company.name, isAvulso: s.company.isAvulso ?? false }
: undefined,
category: s.category ?? undefined, category: s.category ?? undefined,
subcategory: s.subcategory ?? undefined, subcategory: s.subcategory ?? undefined,
lastTimelineEntry: s.lastTimelineEntry ?? undefined, lastTimelineEntry: s.lastTimelineEntry ?? undefined,